diff --git a/packages/bruno-cli/src/runner/interpolate-vars.js b/packages/bruno-cli/src/runner/interpolate-vars.js index c354569939..39e92a6ecd 100644 --- a/packages/bruno-cli/src/runner/interpolate-vars.js +++ b/packages/bruno-cli/src/runner/interpolate-vars.js @@ -1,5 +1,6 @@ const { interpolate } = require('@usebruno/common'); const { each, forOwn, cloneDeep, find } = require('lodash'); +const FormData = require('form-data'); const getContentType = (headers = {}) => { let contentType = ''; @@ -78,6 +79,14 @@ const interpolateVars = (request, envVars = {}, runtimeVariables = {}, processEn request.data = JSON.parse(parsed); } catch (err) {} } + } else if (contentType === 'multipart/form-data') { + if (typeof request.data === 'object' && !(request?.data instanceof FormData)) { + try { + let parsed = JSON.stringify(request.data); + parsed = _interpolate(parsed); + request.data = JSON.parse(parsed); + } catch (err) {} + } } else { request.data = _interpolate(request.data); } diff --git a/packages/bruno-cli/src/runner/prepare-request.js b/packages/bruno-cli/src/runner/prepare-request.js index e30f8337f3..8ba86472b3 100644 --- a/packages/bruno-cli/src/runner/prepare-request.js +++ b/packages/bruno-cli/src/runner/prepare-request.js @@ -120,16 +120,10 @@ const prepareRequest = (request, collectionRoot) => { } if (request.body.mode === 'multipartForm') { + axiosRequest.headers['content-type'] = 'multipart/form-data'; const params = {}; const enabledParams = filter(request.body.multipartForm, (p) => p.enabled); - each(enabledParams, (p) => { - if (p.type === 'file') { - params[p.name] = p.value.map((path) => fs.createReadStream(path)); - } else { - params[p.name] = p.value; - } - }); - axiosRequest.headers['content-type'] = 'multipart/form-data'; + each(enabledParams, (p) => (params[p.name] = p.value)); axiosRequest.data = params; } diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index b260f6be97..c36a9b97f6 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -19,6 +19,7 @@ const { makeAxiosInstance } = require('../utils/axios-instance'); const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-helper'); const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../utils/proxy-util'); const path = require('path'); +const { createFormData } = require('../utils/common'); const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/; const onConsoleLog = (type, args) => { @@ -45,21 +46,6 @@ const runSingleRequest = async function ( const scriptingConfig = get(brunoConfig, 'scripts', {}); scriptingConfig.runtime = runtime; - // make axios work in node using form data - // reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427 - if (request.headers && request.headers['content-type'] === 'multipart/form-data') { - const form = new FormData(); - forOwn(request.data, (value, key) => { - if (value instanceof Array) { - each(value, (v) => form.append(key, v)); - } else { - form.append(key, value); - } - }); - extend(request.headers, form.getHeaders()); - request.data = form; - } - // run pre request script const requestScriptFile = compact([ get(collectionRoot, 'request.script.req'), @@ -195,6 +181,14 @@ const runSingleRequest = async function ( request.data = qs.stringify(request.data); } + if (request?.headers?.['content-type'] === 'multipart/form-data') { + if (!(request?.data instanceof FormData)) { + let form = createFormData(request.data, collectionPath); + request.data = form; + extend(request.headers, form.getHeaders()); + } + } + let response, responseTime; try { // run request diff --git a/packages/bruno-cli/src/utils/common.js b/packages/bruno-cli/src/utils/common.js index 704928022f..16c2d1a7bf 100644 --- a/packages/bruno-cli/src/utils/common.js +++ b/packages/bruno-cli/src/utils/common.js @@ -1,3 +1,8 @@ +const fs = require('fs'); +const FormData = require('form-data'); +const { forOwn } = require('lodash'); +const path = require('path'); + const lpad = (str, width) => { let paddedStr = str; while (paddedStr.length < width) { @@ -14,7 +19,33 @@ const rpad = (str, width) => { return paddedStr; }; +const createFormData = (datas, collectionPath) => { + // make axios work in node using form data + // reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427 + const form = new FormData(); + forOwn(datas, (value, key) => { + if (typeof value == 'string') { + form.append(key, value); + return; + } + + const filePaths = value || []; + filePaths?.forEach?.((filePath) => { + let trimmedFilePath = filePath.trim(); + + if (!path.isAbsolute(trimmedFilePath)) { + trimmedFilePath = path.join(collectionPath, trimmedFilePath); + } + + form.append(key, fs.createReadStream(trimmedFilePath), path.basename(trimmedFilePath)); + }); + }); + return form; +}; + + module.exports = { lpad, - rpad + rpad, + createFormData }; diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 768505e271..b3a41ac27f 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -9,7 +9,7 @@ const decomment = require('decomment'); const contentDispositionParser = require('content-disposition'); const mime = require('mime-types'); const { ipcMain } = require('electron'); -const { isUndefined, isNull, each, get, compact, cloneDeep } = require('lodash'); +const { isUndefined, isNull, each, get, compact, cloneDeep, forOwn, extend } = require('lodash'); const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime } = require('@usebruno/js'); const prepareRequest = require('./prepare-request'); const prepareCollectionRequest = require('./prepare-collection-request'); @@ -37,6 +37,8 @@ const { } = require('./oauth2-helper'); const Oauth2Store = require('../../store/oauth2'); const iconv = require('iconv-lite'); +const FormData = require('form-data'); +const { createFormData } = prepareRequest; const safeStringifyJSON = (data) => { try { @@ -408,6 +410,14 @@ const registerNetworkIpc = (mainWindow) => { request.data = qs.stringify(request.data); } + if (request.headers['content-type'] === 'multipart/form-data') { + if (!(request.data instanceof FormData)) { + let form = createFormData(request.data, collectionPath); + request.data = form; + extend(request.headers, form.getHeaders()); + } + } + return scriptResult; }; diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js index b6aeaa078f..da1c9bab35 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -1,5 +1,6 @@ const { interpolate } = require('@usebruno/common'); const { each, forOwn, cloneDeep, find } = require('lodash'); +const FormData = require('form-data'); const getContentType = (headers = {}) => { let contentType = ''; @@ -76,6 +77,14 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc request.data = JSON.parse(parsed); } catch (err) {} } + } else if (contentType === 'multipart/form-data') { + if (typeof request.data === 'object' && !(request.data instanceof FormData)) { + try { + let parsed = JSON.stringify(request.data); + parsed = _interpolate(parsed); + request.data = JSON.parse(parsed); + } catch (err) {} + } } else { request.data = _interpolate(request.data); } diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index 6601347baf..40c74023c8 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -1,5 +1,5 @@ const os = require('os'); -const { get, each, filter, extend, compact } = require('lodash'); +const { get, each, filter, compact, forOwn } = require('lodash'); const decomment = require('decomment'); const FormData = require('form-data'); const fs = require('fs'); @@ -165,27 +165,26 @@ const mergeFolderLevelScripts = (request, requestTreePath, scriptFlow) => { } }; -const parseFormData = (datas, collectionPath) => { +const createFormData = (datas, collectionPath) => { // make axios work in node using form data // reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427 const form = new FormData(); - datas.forEach((item) => { - const value = item.value; - const name = item.name; - if (item.type === 'file') { - const filePaths = value || []; - filePaths.forEach((filePath) => { - let trimmedFilePath = filePath.trim(); - - if (!path.isAbsolute(trimmedFilePath)) { - trimmedFilePath = path.join(collectionPath, trimmedFilePath); - } - - form.append(name, fs.createReadStream(trimmedFilePath), path.basename(trimmedFilePath)); - }); - } else { - form.append(name, value); + forOwn(datas, (value, key) => { + if (typeof value == 'string') { + form.append(key, value); + return; } + + const filePaths = value || []; + filePaths?.forEach?.((filePath) => { + let trimmedFilePath = filePath.trim(); + + if (!path.isAbsolute(trimmedFilePath)) { + trimmedFilePath = path.join(collectionPath, trimmedFilePath); + } + + form.append(key, fs.createReadStream(trimmedFilePath), path.basename(trimmedFilePath)); + }); }); return form; }; @@ -382,10 +381,11 @@ const prepareRequest = (item, collection) => { } if (request.body.mode === 'multipartForm') { + axiosRequest.headers['content-type'] = 'multipart/form-data'; + const params = {}; const enabledParams = filter(request.body.multipartForm, (p) => p.enabled); - const form = parseFormData(enabledParams, collectionPath); - extend(axiosRequest.headers, form.getHeaders()); - axiosRequest.data = form; + each(enabledParams, (p) => (params[p.name] = p.value)); + axiosRequest.data = params; } if (request.body.mode === 'graphql') { @@ -415,3 +415,4 @@ const prepareRequest = (item, collection) => { module.exports = prepareRequest; module.exports.setAuthHeaders = setAuthHeaders; +module.exports.createFormData = createFormData; diff --git a/packages/bruno-tests/collection/bruno.json b/packages/bruno-tests/collection/bruno.json index b6d437bbb3..ada36145a9 100644 --- a/packages/bruno-tests/collection/bruno.json +++ b/packages/bruno-tests/collection/bruno.json @@ -15,7 +15,7 @@ "bypassProxy": "" }, "scripts": { - "moduleWhitelist": ["crypto", "buffer"], + "moduleWhitelist": ["crypto", "buffer", "form-data"], "filesystemAccess": { "allow": true } diff --git a/packages/bruno-tests/collection/bruno.png b/packages/bruno-tests/collection/bruno.png new file mode 100644 index 0000000000..c2a7f878fb Binary files /dev/null and b/packages/bruno-tests/collection/bruno.png differ diff --git a/packages/bruno-tests/collection/echo/echo form-url-encoded.bru b/packages/bruno-tests/collection/echo/echo form-url-encoded.bru new file mode 100644 index 0000000000..a0d2f0afbd --- /dev/null +++ b/packages/bruno-tests/collection/echo/echo form-url-encoded.bru @@ -0,0 +1,23 @@ +meta { + name: echo form-url-encoded + type: http + seq: 9 +} + +post { + url: {{echo-host}} + body: formUrlEncoded + auth: none +} + +body:form-urlencoded { + form-data-key: {{form-data-key}} +} + +script:pre-request { + bru.setVar('form-data-key', 'form-data-value'); +} + +assert { + res.body: eq form-data-key=form-data-value +} diff --git a/packages/bruno-tests/collection/echo/echo multipart scripting.bru b/packages/bruno-tests/collection/echo/echo multipart scripting.bru new file mode 100644 index 0000000000..13c1f20515 --- /dev/null +++ b/packages/bruno-tests/collection/echo/echo multipart scripting.bru @@ -0,0 +1,22 @@ +meta { + name: echo multipart via scripting + type: http + seq: 10 +} + +post { + url: {{echo-host}} + body: multipartForm + auth: none +} + +assert { + res.body: contains form-data-value +} + +script:pre-request { + const FormData = require("form-data"); + const form = new FormData(); + form.append('form-data-key', 'form-data-value'); + req.setBody(form); +} diff --git a/packages/bruno-tests/collection/echo/echo multipart.bru b/packages/bruno-tests/collection/echo/echo multipart.bru new file mode 100644 index 0000000000..b8fd8abf7a --- /dev/null +++ b/packages/bruno-tests/collection/echo/echo multipart.bru @@ -0,0 +1,24 @@ +meta { + name: echo multipart + type: http + seq: 8 +} + +post { + url: {{echo-host}} + body: multipartForm + auth: none +} + +body:multipart-form { + foo: {{form-data-key}} + file: @file(bruno.png) +} + +assert { + res.body: contains form-data-value +} + +script:pre-request { + bru.setVar('form-data-key', 'form-data-value'); +} diff --git a/packages/bruno-tests/collection/environments/Prod.bru b/packages/bruno-tests/collection/environments/Prod.bru index 4bea1e77a5..ce8fa60cca 100644 --- a/packages/bruno-tests/collection/environments/Prod.bru +++ b/packages/bruno-tests/collection/environments/Prod.bru @@ -7,4 +7,5 @@ vars { bark: {{process.env.PROC_ENV_VAR}} foo: bar testSetEnvVar: bruno-29653 + echo-host: https://echo.usebruno.com }