Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: multipart/form-data body interpolation #3142

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two if-statements can be merged.

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 @@ -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;
};

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 @@ -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') {
Expand Down Expand Up @@ -415,3 +415,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"],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could add form-data to the default libraries.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since form-data dep needs to be added to both vm2 and quickjs, i'l create a seperate pr for just that

"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
}