From ed20eccc25d627a67e00fec944b5ec1daf7d8484 Mon Sep 17 00:00:00 2001 From: lohit Date: Mon, 23 Sep 2024 15:54:54 +0530 Subject: [PATCH] fix: multipart/form-data body interpolation (#3142) * feat: updates * feat: updates * feat: updates * feat: updates --- .../bruno-cli/src/runner/interpolate-vars.js | 9 ++++ .../bruno-cli/src/runner/prepare-request.js | 10 +--- .../src/runner/run-single-request.js | 24 ++++------ packages/bruno-cli/src/utils/common.js | 33 +++++++++++++- .../bruno-electron/src/ipc/network/index.js | 12 ++++- .../src/ipc/network/interpolate-vars.js | 9 ++++ .../src/ipc/network/prepare-request.js | 43 +++++++++--------- packages/bruno-tests/collection/bruno.json | 2 +- packages/bruno-tests/collection/bruno.png | Bin 0 -> 795 bytes .../collection/echo/echo form-url-encoded.bru | 23 ++++++++++ .../echo/echo multipart scripting.bru | 22 +++++++++ .../collection/echo/echo multipart.bru | 24 ++++++++++ .../collection/environments/Prod.bru | 1 + 13 files changed, 165 insertions(+), 47 deletions(-) create mode 100644 packages/bruno-tests/collection/bruno.png create mode 100644 packages/bruno-tests/collection/echo/echo form-url-encoded.bru create mode 100644 packages/bruno-tests/collection/echo/echo multipart scripting.bru create mode 100644 packages/bruno-tests/collection/echo/echo multipart.bru diff --git a/packages/bruno-cli/src/runner/interpolate-vars.js b/packages/bruno-cli/src/runner/interpolate-vars.js index c35456993..39e92a6ec 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 e30f8337f..8ba86472b 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 b260f6be9..c36a9b97f 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 704928022..16c2d1a7b 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 d7ed96d28..b0dfbfb68 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 { @@ -423,6 +425,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 b6aeaa078..da1c9bab3 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 75b0f2c0e..61bbd7a30 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; }; @@ -400,10 +399,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') { @@ -433,3 +433,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 b6d437bbb..ada36145a 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 0000000000000000000000000000000000000000..c2a7f878fb5bd695f7b67db54fa0ad121740d862 GIT binary patch literal 795 zcmV+$1LXXPP)`Dq^c7q<0*i%&IgAFX`BKTkzNHoGr zR-lAudQ$U^D&V=)7()e5)U9m8?lMzvb4%FfQV z*VoriUGE{*s>+Q?DHk;wO`l9A!*2w? z|pt-p@u=3`Grm_=J zanVXC7((zbshCAVOc-{rlBllUeV$O)*j!#zR76<_x;j5*7o-OrQOHI3A8{aqWs%In z(}jis!CS^;GD+0)9A~1HQnYqDK`HRy#W#GldobP2h5-zXxbeQct`OV+npr zT_1tSut4}Ig^yDBWu+()Bx2#6q!a*2ik*YDwzdu2BxT(Ah2oF8mSLQ|X9YlUmqRb$sk6)dk`Y# zVEgXHv)6JAhHzG~3~NPl zRGWv8wQ~XT>iC*PD>ff+(Xfhwf`S1E2>zkfYQx6*TP{xZ_Z`(LmV{6WTycGJ*hQ+c z)We!{X=!N_n1)~$>Xor4j|Y>&G4CV{!I7y(qfv($Fqp;ts_N_2f$~J1NS<6