Skip to content

Commit

Permalink
Merge pull request #2121 from end3rbyte/feature/1602-multipart-conten…
Browse files Browse the repository at this point in the history
…t-type

feature: Multi-part requests: user should be able to set content-type for each part in a multi-part request. #1602
  • Loading branch information
lohxt1 authored Dec 15, 2024
2 parents 366bd99 + 73ea5f1 commit 83bbbe3
Show file tree
Hide file tree
Showing 18 changed files with 198 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ const StyledWrapper = styled.div`
line-height: 30px;
overflow: hidden;
pre.CodeMirror-placeholder {
color: ${(props) => props.theme.text};
padding-left: 0;
opacity: 0.5;
}
.CodeMirror-scroll {
overflow: hidden !important;
${'' /* padding-bottom: 50px !important; */}
Expand Down
1 change: 1 addition & 0 deletions packages/bruno-app/src/components/MultiLineEditor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class MultiLineEditor extends Component {
lineWrapping: false,
lineNumbers: false,
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
placeholder: this.props.placeholder,
mode: 'brunovariables',
brunoVarInfo: {
variables
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ const Wrapper = styled.div`
td {
padding: 6px 10px;
&:nth-child(1) {
width: 30%;
&:nth-child(2) {
width: 130px;
}
&:nth-child(4) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,15 @@ const Wrapper = styled.div`
width: 30%;
}
&:nth-child(2) {
width: 45%;
}
&:nth-child(3) {
width: 25%;
}
&:nth-child(4) {
width: 70px;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,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 @@ -85,6 +89,7 @@ const MultipartFormParams = ({ item, collection }) => {
<tr>
<td>Key</td>
<td>Value</td>
<td>Content-Type</td>
<td></td>
</tr>
</thead>
Expand Down Expand Up @@ -145,6 +150,27 @@ const MultipartFormParams = ({ item, collection }) => {
/>
)}
</td>
<td>
<MultiLineEditor
onSave={onSave}
theme={storedTheme}
placeholder="Auto"
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
1 change: 1 addition & 0 deletions packages/bruno-app/src/pages/Bruno/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ if (!SERVER_RENDERED) {
require('codemirror/addon/search/jump-to-line');
require('codemirror/addon/search/search');
require('codemirror/addon/search/searchcursor');
require('codemirror/addon/display/placeholder');
require('codemirror/keymap/sublime');

require('codemirror-graphql/hint');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,7 @@ export const collectionsSlice = createSlice({
name: '',
value: action.payload.value,
description: '',
contentType: '',
enabled: true
});
}
Expand All @@ -786,6 +787,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
2 changes: 1 addition & 1 deletion packages/bruno-cli/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const EXIT_STATUS = {
// Invalid output format requested
ERROR_INCORRECT_OUTPUT_FORMAT: 9,
// Everything else
ERROR_GENERIC: 255,
ERROR_GENERIC: 255
};

module.exports = {
Expand Down
36 changes: 31 additions & 5 deletions packages/bruno-cli/src/runner/prepare-request.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,37 @@
const { get, each, filter, find, compact } = require('lodash');
const { get, each, filter } = require('lodash');
const decomment = require('decomment');
const crypto = require('node:crypto');
const { mergeHeaders, mergeScripts, mergeVars, getTreePathFromCollectionToItem } = require('../utils/collection');

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;
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 = (item = {}, collection = {}) => {
const request = item?.request;
const brunoConfig = get(collection, 'brunoConfig', {});
Expand Down Expand Up @@ -132,13 +160,11 @@ const prepareRequest = (item = {}, collection = {}) => {
each(enabledParams, (p) => (params[p.name] = p.value));
axiosRequest.data = params;
}

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) => (params[p.name] = p.value));
axiosRequest.data = params;
axiosRequest.data = createFormData(enabledParams);
}

if (request.body.mode === 'graphql') {
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 @@ -46,7 +46,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 @@ -160,16 +160,31 @@ const mapRequestParams = (pairList = [], type) => {
});
};

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
7 changes: 5 additions & 2 deletions packages/bruno-lang/v2/src/jsonToBru.js
Original file line number Diff line number Diff line change
Expand Up @@ -280,15 +280,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}: ${getValueString(item.value)}`;
return `${enabled}${item.name}: ${getValueString(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 @@ -117,18 +117,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
24 changes: 24 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,24 @@
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)
}

assert {
res.status: eq 200
res.body.find(p=>p.name === 'param1').contentType: isUndefined
res.body.find(p=>p.name === 'param2').contentType: eq application/json
res.body.find(p=>p.name === 'param3').contentType: eq 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.
9 changes: 3 additions & 6 deletions packages/bruno-tests/src/index.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const multer = require('multer');
const formDataParser = require('./multipart/form-data-parser');
const authRouter = require('./auth');
const echoRouter = require('./echo');
const xmlParser = require('./utils/xmlParser');

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);

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 @@ -31,10 +32,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 83bbbe3

Please sign in to comment.