Skip to content

Commit

Permalink
fix: multipart/form-data body interpolation (usebruno#3142)
Browse files Browse the repository at this point in the history
* feat: updates

* feat: updates

* feat: updates

* feat: updates
  • Loading branch information
lohxt1 authored Sep 23, 2024
1 parent eb33504 commit ed20ecc
Show file tree
Hide file tree
Showing 13 changed files with 165 additions and 47 deletions.
9 changes: 9 additions & 0 deletions packages/bruno-cli/src/runner/interpolate-vars.js
Original file line number Diff line number Diff line change
@@ -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 = '';
Expand Down Expand Up @@ -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);
}
Expand Down
10 changes: 2 additions & 8 deletions packages/bruno-cli/src/runner/prepare-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
24 changes: 9 additions & 15 deletions packages/bruno-cli/src/runner/run-single-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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'),
Expand Down Expand Up @@ -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
Expand Down
33 changes: 32 additions & 1 deletion packages/bruno-cli/src/utils/common.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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
};
12 changes: 11 additions & 1 deletion packages/bruno-electron/src/ipc/network/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
};

Expand Down
9 changes: 9 additions & 0 deletions packages/bruno-electron/src/ipc/network/interpolate-vars.js
Original file line number Diff line number Diff line change
@@ -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 = '';
Expand Down Expand Up @@ -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);
}
Expand Down
43 changes: 22 additions & 21 deletions packages/bruno-electron/src/ipc/network/prepare-request.js
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -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;
};
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -433,3 +433,4 @@ const prepareRequest = (item, collection) => {

module.exports = prepareRequest;
module.exports.setAuthHeaders = setAuthHeaders;
module.exports.createFormData = createFormData;
2 changes: 1 addition & 1 deletion packages/bruno-tests/collection/bruno.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"bypassProxy": ""
},
"scripts": {
"moduleWhitelist": ["crypto", "buffer"],
"moduleWhitelist": ["crypto", "buffer", "form-data"],
"filesystemAccess": {
"allow": true
}
Expand Down
Binary file added packages/bruno-tests/collection/bruno.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions packages/bruno-tests/collection/echo/echo form-url-encoded.bru
Original file line number Diff line number Diff line change
@@ -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
}
22 changes: 22 additions & 0 deletions packages/bruno-tests/collection/echo/echo multipart scripting.bru
Original file line number Diff line number Diff line change
@@ -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);
}
24 changes: 24 additions & 0 deletions packages/bruno-tests/collection/echo/echo multipart.bru
Original file line number Diff line number Diff line change
@@ -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');
}
1 change: 1 addition & 0 deletions packages/bruno-tests/collection/environments/Prod.bru
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ vars {
bark: {{process.env.PROC_ENV_VAR}}
foo: bar
testSetEnvVar: bruno-29653
echo-host: https://echo.usebruno.com
}

0 comments on commit ed20ecc

Please sign in to comment.