diff --git a/packages/bruno-app/src/components/MultiLineEditor/StyledWrapper.js b/packages/bruno-app/src/components/MultiLineEditor/StyledWrapper.js
index 50c806d9f0..2473463db3 100644
--- a/packages/bruno-app/src/components/MultiLineEditor/StyledWrapper.js
+++ b/packages/bruno-app/src/components/MultiLineEditor/StyledWrapper.js
@@ -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; */}
diff --git a/packages/bruno-app/src/components/MultiLineEditor/index.js b/packages/bruno-app/src/components/MultiLineEditor/index.js
index 97b3eca07a..a44caf4ba8 100644
--- a/packages/bruno-app/src/components/MultiLineEditor/index.js
+++ b/packages/bruno-app/src/components/MultiLineEditor/index.js
@@ -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
diff --git a/packages/bruno-app/src/components/RequestPane/Assertions/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Assertions/StyledWrapper.js
index 6f28fee13b..14e21e1c60 100644
--- a/packages/bruno-app/src/components/RequestPane/Assertions/StyledWrapper.js
+++ b/packages/bruno-app/src/components/RequestPane/Assertions/StyledWrapper.js
@@ -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) {
diff --git a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/StyledWrapper.js
index f04a30be0f..49c45f21dc 100644
--- a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/StyledWrapper.js
+++ b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/StyledWrapper.js
@@ -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;
}
}
diff --git a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js
index 0d57d97eae..af23a645e6 100644
--- a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js
+++ b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js
@@ -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;
@@ -85,6 +89,7 @@ const MultipartFormParams = ({ item, collection }) => {
Key |
Value |
+ Content-Type |
|
@@ -145,6 +150,27 @@ const MultipartFormParams = ({ item, collection }) => {
/>
)}
+
+
+ handleParamChange(
+ {
+ target: {
+ value: newValue
+ }
+ },
+ param,
+ 'contentType'
+ )
+ }
+ onRun={handleRun}
+ collection={collection}
+ />
+ |
{
+ // 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', {});
@@ -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') {
diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js
index c84d36d073..1324438900 100644
--- a/packages/bruno-lang/v2/src/bruToJson.js
+++ b/packages/bruno-lang/v2/src/bruToJson.js
@@ -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)*
@@ -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;
});
};
diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js
index 8d3a5fdee8..03e36c71a8 100644
--- a/packages/bruno-lang/v2/src/jsonToBru.js
+++ b/packages/bruno-lang/v2/src/jsonToBru.js
@@ -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')
diff --git a/packages/bruno-lang/v2/tests/fixtures/request.json b/packages/bruno-lang/v2/tests/fixtures/request.json
index 24997a90c7..9c8ed143da 100644
--- a/packages/bruno-lang/v2/tests/fixtures/request.json
+++ b/packages/bruno-lang/v2/tests/fixtures/request.json
@@ -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,
diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js
index 11561c5284..3d5959f15d 100644
--- a/packages/bruno-schema/src/collections/index.js
+++ b/packages/bruno-schema/src/collections/index.js
@@ -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)
diff --git a/packages/bruno-tests/collection/multipart/mixed-content-types.bru b/packages/bruno-tests/collection/multipart/mixed-content-types.bru
new file mode 100644
index 0000000000..45a1cdd184
--- /dev/null
+++ b/packages/bruno-tests/collection/multipart/mixed-content-types.bru
@@ -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
+}
diff --git a/packages/bruno-tests/collection/multipart/small.png b/packages/bruno-tests/collection/multipart/small.png
new file mode 100644
index 0000000000..2b584adf05
Binary files /dev/null and b/packages/bruno-tests/collection/multipart/small.png differ
diff --git a/packages/bruno-tests/src/index.js b/packages/bruno-tests/src/index.js
index a6c72dc2b3..0bff6ab752 100644
--- a/packages/bruno-tests/src/index.js
+++ b/packages/bruno-tests/src/index.js
@@ -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');
@@ -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');
});
diff --git a/packages/bruno-tests/src/multipart/form-data-parser.js b/packages/bruno-tests/src/multipart/form-data-parser.js
new file mode 100644
index 0000000000..8b4aa500ad
--- /dev/null
+++ b/packages/bruno-tests/src/multipart/form-data-parser.js
@@ -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;
diff --git a/packages/bruno-tests/src/multipart/index.js b/packages/bruno-tests/src/multipart/index.js
new file mode 100644
index 0000000000..a98837c54a
--- /dev/null
+++ b/packages/bruno-tests/src/multipart/index.js
@@ -0,0 +1,10 @@
+const express = require('express');
+const router = express.Router();
+const formDataParser = require('./form-data-parser');
+
+router.post('/mixed-content-types', (req, res) => {
+ const parts = formDataParser.parse(req);
+ return res.json(parts);
+});
+
+module.exports = router;
|