Skip to content

Commit

Permalink
feature: Multi-part requests: user should be able to set content-type…
Browse files Browse the repository at this point in the history
… for each part in a multi-part request. usebruno#1602
  • Loading branch information
busy-panda committed Apr 18, 2024
1 parent d027d90 commit 88df962
Show file tree
Hide file tree
Showing 15 changed files with 209 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const Info = ({ collection }) => {
</tr>
<tr className="">
<td className="py-2 px-2 text-right">Ignored files&nbsp;:</td>
<td className="py-2 px-2 break-all">{collection.brunoConfig?.ignore?.map((x) => `'${x}'`).join(', ')}</td>
<td className="py-2 px-2 break-all">{collection.brunoConfig.ignore.map((x) => `'${x}'`).join(', ')}</td>
</tr>
<tr className="">
<td className="py-2 px-2 text-right">Environments&nbsp;:</td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const Wrapper = styled.div`
width: 30%;
}
&:nth-child(3) {
&:nth-child(4) {
width: 70px;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ const MultipartFormParams = ({ item, collection }) => {
param.value = e.target.value;
break;
}
case 'contentType': {
param.contentType = e.target.value;
break;
}
case 'enabled': {
param.enabled = e.target.checked;
break;
Expand Down Expand Up @@ -83,6 +87,7 @@ const MultipartFormParams = ({ item, collection }) => {
<tr>
<td>Key</td>
<td>Value</td>
<td>Content-Type</td>
<td></td>
</tr>
</thead>
Expand Down Expand Up @@ -142,6 +147,26 @@ const MultipartFormParams = ({ item, collection }) => {
/>
)}
</td>
<td>
<MultiLineEditor
onSave={onSave}
theme={storedTheme}
value={param.contentType}
onChange={(newValue) =>
handleParamChange(
{
target: {
value: newValue
}
},
param,
'contentType'
)
}
onRun={handleRun}
collection={collection}
/>
</td>
<td>
<div className="flex items-center">
<input
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,7 @@ export const collectionsSlice = createSlice({
name: '',
value: '',
description: '',
contentType: '',
enabled: true
});
}
Expand All @@ -646,6 +647,7 @@ export const collectionsSlice = createSlice({
param.name = action.payload.param.name;
param.value = action.payload.param.value;
param.description = action.payload.param.description;
param.contentType = action.payload.param.contentType;
param.enabled = action.payload.param.enabled;
}
}
Expand Down
51 changes: 38 additions & 13 deletions packages/bruno-cli/src/runner/prepare-request.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,38 @@
const { get, each, filter } = require('lodash');
const fs = require('fs');
var JSONbig = require('json-bigint');
const { get, each, filter, extend } = require('lodash');
const decomment = require('decomment');
var JSONbig = require('json-bigint');
const FormData = require('form-data');
const fs = require('fs');
const path = require('path');

const parseFormData = (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;
let options = {};
if (item.contentType) {
options.contentType = item.contentType;
}
if (item.type === 'file') {
const filePaths = value || [];
filePaths.forEach((filePath) => {
let trimmedFilePath = filePath.trim();

if (!path.isAbsolute(trimmedFilePath)) {
trimmedFilePath = path.join(collectionPath, trimmedFilePath);
}
options.filename = path.basename(trimmedFilePath);
form.append(name, fs.createReadStream(trimmedFilePath), options);
});
} else {
form.append(name, value, options);
}
});
return form;
};

const prepareRequest = (request, collectionRoot) => {
const headers = {};
Expand Down Expand Up @@ -124,17 +155,11 @@ const prepareRequest = (request, collectionRoot) => {
}

if (request.body.mode === 'multipartForm') {
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';
axiosRequest.data = params;
const collectionPath = process.cwd();
const form = parseFormData(enabledParams, collectionPath);
extend(axiosRequest.headers, form.getHeaders());
axiosRequest.data = form;
}

if (request.body.mode === 'graphql') {
Expand Down
10 changes: 7 additions & 3 deletions packages/bruno-electron/src/ipc/network/prepare-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ const parseFormData = (datas, collectionPath) => {
datas.forEach((item) => {
const value = item.value;
const name = item.name;
let options = {};
if (item.contentType) {
options.contentType = item.contentType;
}
if (item.type === 'file') {
const filePaths = value || [];
filePaths.forEach((filePath) => {
Expand All @@ -20,11 +24,11 @@ const parseFormData = (datas, collectionPath) => {
if (!path.isAbsolute(trimmedFilePath)) {
trimmedFilePath = path.join(collectionPath, trimmedFilePath);
}

form.append(name, fs.createReadStream(trimmedFilePath), path.basename(trimmedFilePath));
options.filename = path.basename(trimmedFilePath);
form.append(name, fs.createReadStream(trimmedFilePath), options);
});
} else {
form.append(name, value);
form.append(name, value, options);
}
});
return form;
Expand Down
17 changes: 16 additions & 1 deletion packages/bruno-lang/v2/src/bruToJson.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const grammar = ohm.grammar(`Bru {
pair = st* key st* ":" st* value st*
key = keychar*
value = multilinetextblock | valuechar*
// Dictionary for Assert Block
assertdictionary = st* "{" assertpairlist? tagend
assertpairlist = optionalnl* assertpair (~tagend stnl* assertpair)* (~tagend space)*
Expand Down Expand Up @@ -133,16 +133,31 @@ const mapPairListToKeyValPairs = (pairList = [], parseEnabled = true) => {
});
};

const multipartExtractContentType = (pair) => {
if (_.isString(pair.value)) {
const match = pair.value.match(/^(.*?)\s*\(Content-Type=(.*?)\)\s*$/);
if (match != null && match.length > 2) {
pair.value = match[1];
pair.contentType = match[2];
} else {
pair.contentType = '';
}
}
};

const mapPairListToKeyValPairsMultipart = (pairList = [], parseEnabled = true) => {
const pairs = mapPairListToKeyValPairs(pairList, parseEnabled);

return pairs.map((pair) => {
pair.type = 'text';
multipartExtractContentType(pair);

if (pair.value.startsWith('@file(') && pair.value.endsWith(')')) {
let filestr = pair.value.replace(/^@file\(/, '').replace(/\)$/, '');
pair.type = 'file';
pair.value = filestr.split('|');
}

return pair;
});
};
Expand Down
6 changes: 4 additions & 2 deletions packages/bruno-lang/v2/src/jsonToBru.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,16 +247,18 @@ ${indentString(body.sparql)}
multipartForms
.map((item) => {
const enabled = item.enabled ? '' : '~';
const contentType =
item.contentType && item.contentType !== '' ? ' (Content-Type=' + item.contentType + ')' : '';
if (item.type === 'text') {
return `${enabled}${item.name}: ${item.value}`;
return `${enabled}${item.name}: ${item.value}${contentType}`;
}
if (item.type === 'file') {
let filepaths = item.value || [];
let filestr = filepaths.join('|');
const value = `@file(${filestr})`;
return `${enabled}${item.name}: ${value}`;
return `${enabled}${item.name}: ${value}${contentType}`;
}
})
.join('\n')
Expand Down
3 changes: 3 additions & 0 deletions packages/bruno-lang/v2/tests/fixtures/request.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,18 +103,21 @@
],
"multipartForm": [
{
"contentType": "",
"name": "apikey",
"value": "secret",
"enabled": true,
"type": "text"
},
{
"contentType": "",
"name": "numbers",
"value": "+91998877665",
"enabled": true,
"type": "text"
},
{
"contentType": "",
"name": "message",
"value": "hello",
"enabled": false,
Expand Down
1 change: 1 addition & 0 deletions packages/bruno-schema/src/collections/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const multipartFormSchema = Yup.object({
otherwise: Yup.string().nullable()
}),
description: Yup.string().nullable(),
contentType: Yup.string().nullable(),
enabled: Yup.boolean()
})
.noUnknown(true)
Expand Down
39 changes: 39 additions & 0 deletions packages/bruno-tests/collection/multipart/mixed-content-types.bru
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
meta {
name: mixed-content-types
type: http
seq: 1
}

post {
url: {{host}}/api/multipart/mixed-content-types
body: multipartForm
auth: none
}

body:multipart-form {
param1: test
param2: {"test":"i am json"} (Content-Type=application/json)
param3: @file(multipart/small.png)
}

tests {
test("Status code is 200", function () {
expect(res.getStatus()).to.equal(200);
});
test("param1 has no content-type", function () {
var param1 = res.body.find(p=>p.name === 'param1')
expect(param1).to.be.an('object');
expect(param1.contentType).to.be.undefined;
});
test("param2 has content-type application/json", function () {
var param2 = res.body.find(p=>p.name === 'param2')
expect(param2).to.be.an('object');
expect(param2.contentType).to.equals('application/json');
});
test("param3 has content-type image/png", function () {
var param3 = res.body.find(p=>p.name === 'param3')
expect(param3).to.be.an('object');
expect(param3.contentType).to.equals('image/png');
});

}
Binary file added packages/bruno-tests/collection/multipart/small.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 4 additions & 6 deletions packages/bruno-tests/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,25 @@ const express = require('express');
const bodyParser = require('body-parser');
const xmlparser = require('express-xml-bodyparser');
const cors = require('cors');
const multer = require('multer');
const formDataParser = require('./multipart/form-data-parser');

const app = new express();
const port = process.env.PORT || 8080;
const upload = multer();

app.use(cors());
app.use(xmlparser());
app.use(bodyParser.text());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
formDataParser.init(app, express);

const authRouter = require('./auth');
const echoRouter = require('./echo');
const multipartRouter = require('./multipart');

app.use('/api/auth', authRouter);
app.use('/api/echo', echoRouter);
app.use('/api/multipart', multipartRouter);

app.get('/ping', function (req, res) {
return res.send('pong');
Expand All @@ -32,10 +34,6 @@ app.get('/query', function (req, res) {
return res.json(req.query);
});

app.post('/echo/multipartForm', upload.none(), function (req, res) {
return res.json(req.body);
});

app.get('/redirect-to-ping', function (req, res) {
return res.redirect('/ping');
});
Expand Down
58 changes: 58 additions & 0 deletions packages/bruno-tests/src/multipart/form-data-parser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* Instead of using multer for example to parse the multipart form data, we build our own parser
* so that we can verify the content type are set correctly by bruno (for example application/json for json content)
*/

const extractParam = function (param, str, delimiter, quote, endDelimiter) {
let regex = new RegExp(`${param}${delimiter}\\s*${quote}(.*?)${quote}${endDelimiter}`);
const found = str.match(regex);
if (found != null && found.length > 1) {
return found[1];
} else {
return null;
}
};

const init = function (app, express) {
app.use(express.raw({ type: 'multipart/form-data' }));
};

const parsePart = function (part) {
let result = {};
const name = extractParam('name', part, '=', '"', '');
if (name) {
result.name = name;
}
const filename = extractParam('filename', part, '=', '"', '');
if (filename) {
result.filename = filename;
}
const contentType = extractParam('Content-Type', part, ':', '', ';');
if (contentType) {
result.contentType = contentType;
}
if (!filename) {
result.value = part.substring(part.indexOf('value=') + 'value='.length);
}
if (contentType === 'application/json') {
result.value = JSON.parse(result.value);
}
return result;
};

const parse = function (req) {
const BOUNDARY = 'boundary=';
const contentType = req.headers['content-type'];
const boundary = '--' + contentType.substring(contentType.indexOf(BOUNDARY) + BOUNDARY.length);
const rawBody = req.body.toString();
let parts = rawBody.split(boundary).filter((part) => part.length > 0);
parts = parts.map((part) => part.trim('\r\n'));
parts = parts.filter((part) => part != '--');
parts = parts.map((part) => part.replace('\r\n\r\n', ';value='));
parts = parts.map((part) => part.replace('\r\n', ';'));
parts = parts.map((part) => parsePart(part));
return parts;
};

module.exports.parse = parse;
module.exports.init = init;
Loading

0 comments on commit 88df962

Please sign in to comment.