mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
fix: multipart/mixed and multipart/form-data interpolation and generic request behaviour (#8087)
* fix: multipart spec additions and interpolation fixes * test(cli): add interpolation multipart tests * Update packages/bruno-tests/collection/multipart/multipart-mixed-form-data-parse.bru Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * chore: remove assert as curl also gives the same result * chore: codestyle --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -102,7 +102,9 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
|
||||
}));
|
||||
}
|
||||
} else if (contentType.startsWith('multipart/')) {
|
||||
if (Array.isArray(request?.data) && !isFormData(request.data)) {
|
||||
if (request?.data && typeof request.data === 'string') {
|
||||
request.data = _interpolate(request.data);
|
||||
} else if (Array.isArray(request?.data) && !isFormData(request.data)) {
|
||||
try {
|
||||
request.data = request?.data?.map((d) => ({
|
||||
...d,
|
||||
|
||||
@@ -487,7 +487,7 @@ const runSingleRequest = async function (
|
||||
|
||||
const contentType = contentTypeHeader ? request.headers[contentTypeHeader] : '';
|
||||
if (typeof contentType === 'string' && contentType.startsWith('multipart/')) {
|
||||
if (!isFormData(request?.data)) {
|
||||
if (typeof request.data !== 'string' && !isFormData(request?.data)) {
|
||||
request._originalMultipartData = request.data;
|
||||
request.collectionPath = collectionPath;
|
||||
let form = createFormData(request.data, collectionPath);
|
||||
|
||||
@@ -18,6 +18,58 @@ describe('interpolate-vars: interpolateVars', () => {
|
||||
const result = interpolateVars(request, { shouldNotApply: 'value' }, null, null);
|
||||
expect(result.data).toBe(streamPayload);
|
||||
});
|
||||
|
||||
it('preserves raw string body when Content-Type is multipart/mixed', () => {
|
||||
const rawMultipartBody = [
|
||||
'--TestBoundary123',
|
||||
'Content-Type: application/json',
|
||||
'',
|
||||
'{"test": true}',
|
||||
'--TestBoundary123--',
|
||||
''
|
||||
].join('\r\n');
|
||||
|
||||
const request = {
|
||||
method: 'POST',
|
||||
mode: 'text',
|
||||
url: 'https://httpbin.dev/post',
|
||||
headers: { 'content-type': 'multipart/mixed; boundary=TestBoundary123' },
|
||||
data: rawMultipartBody
|
||||
};
|
||||
|
||||
const result = interpolateVars(request, {}, null, null);
|
||||
expect(result.data).toBe(rawMultipartBody);
|
||||
});
|
||||
|
||||
it('interpolates variables in raw multipart/mixed string body', () => {
|
||||
const boundary = 'CustomBoundary123';
|
||||
const rawMultipartBody = [
|
||||
`--${boundary}`,
|
||||
'Content-Type: text/plain',
|
||||
'',
|
||||
'Token: {{token}}',
|
||||
`--${boundary}`,
|
||||
'Content-Type: application/json',
|
||||
'',
|
||||
'{"id": "{{id}}", "msg": "{{msg}}"}',
|
||||
`--${boundary}--`,
|
||||
''
|
||||
].join('\r\n');
|
||||
|
||||
const request = {
|
||||
method: 'POST',
|
||||
mode: 'text',
|
||||
url: 'https://api.example/send',
|
||||
headers: { 'content-type': `multipart/mixed; boundary=${boundary}` },
|
||||
data: rawMultipartBody
|
||||
};
|
||||
|
||||
const result = interpolateVars(request, { token: 'abc123', id: 42, msg: 'hello' }, null, null);
|
||||
expect(result.data).toContain('Token: abc123');
|
||||
expect(result.data).toContain('{"id": "42", "msg": "hello"}');
|
||||
expect(result.data).toContain(`--${boundary}`);
|
||||
expect(result.data).toContain(`--${boundary}--`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('interpolate-vars: api key header name sidecar', () => {
|
||||
|
||||
@@ -607,7 +607,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
const contentType = contentTypeHeader ? request.headers[contentTypeHeader] : '';
|
||||
if (typeof contentType === 'string' && contentType.startsWith('multipart/')) {
|
||||
if (!isFormData(request.data)) {
|
||||
if (typeof request.data !== 'string' && !isFormData(request.data)) {
|
||||
request._originalMultipartData = request.data;
|
||||
request.collectionPath = collectionPath;
|
||||
let form = createFormData(request.data, collectionPath);
|
||||
|
||||
@@ -140,7 +140,9 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
|
||||
}));
|
||||
}
|
||||
} else if (contentType.startsWith('multipart/')) {
|
||||
if (Array.isArray(request?.data) && !isFormData(request.data)) {
|
||||
if (request?.data && typeof request.data === 'string') {
|
||||
request.data = _interpolate(request.data);
|
||||
} else if (Array.isArray(request?.data) && !isFormData(request.data)) {
|
||||
try {
|
||||
request.data = request?.data?.map((d) => ({
|
||||
...d,
|
||||
|
||||
@@ -425,6 +425,200 @@ describe('interpolate-vars: interpolateVars', () => {
|
||||
expect(result.data).toContain('{"test": true}');
|
||||
expect(result.data).toContain('--TestBoundary123--');
|
||||
});
|
||||
|
||||
it('interpolates variables in text-based multipart/mixed body with manual boundaries', () => {
|
||||
// User manually constructs a multipart/mixed body as a string
|
||||
const boundary = 'CustomBoundary123';
|
||||
const rawMultipartBody = [
|
||||
`--${boundary}`,
|
||||
'Content-Type: text/plain',
|
||||
'',
|
||||
'Token: {{token}}',
|
||||
`--${boundary}`,
|
||||
'Content-Type: application/json',
|
||||
'',
|
||||
'{"id": "{{id}}", "msg": "{{msg}}"}',
|
||||
`--${boundary}--`,
|
||||
''
|
||||
].join('\r\n');
|
||||
|
||||
const request = {
|
||||
method: 'POST',
|
||||
url: 'https://api.example/send',
|
||||
headers: { 'content-type': `multipart/mixed; boundary=${boundary}` },
|
||||
data: rawMultipartBody
|
||||
};
|
||||
|
||||
const result = interpolateVars(request, { token: 'abc123', id: 42, msg: 'hello' }, null, null);
|
||||
|
||||
expect(result.data).toContain('Token: abc123');
|
||||
expect(result.data).toContain('{"id": "42", "msg": "hello"}');
|
||||
// Ensure boundaries are preserved
|
||||
expect(result.data).toContain(`--${boundary}`);
|
||||
expect(result.data).toContain(`--${boundary}--`);
|
||||
});
|
||||
|
||||
it('interpolates variables in boundary lines themselves', () => {
|
||||
const boundaryVar = 'BoundaryVar';
|
||||
const rawMultipartBody = [
|
||||
`--{{boundary}}`,
|
||||
'Content-Type: text/plain',
|
||||
'',
|
||||
'Hello',
|
||||
`--{{boundary}}--`,
|
||||
''
|
||||
].join('\r\n');
|
||||
const request = {
|
||||
method: 'POST',
|
||||
url: 'https://api.example/send',
|
||||
headers: { 'content-type': 'multipart/mixed; boundary={{boundary}}' },
|
||||
data: rawMultipartBody
|
||||
};
|
||||
const result = interpolateVars(request, { boundary: boundaryVar }, null, null);
|
||||
expect(result.data).toContain(`--${boundaryVar}`);
|
||||
expect(result.data).toContain(`--${boundaryVar}--`);
|
||||
});
|
||||
|
||||
it('interpolates variables that resolve to empty string or undefined', () => {
|
||||
const boundary = 'B';
|
||||
const rawMultipartBody = [
|
||||
`--${boundary}`,
|
||||
'Content-Type: text/plain',
|
||||
'',
|
||||
'Token: {{missingVar}}',
|
||||
`--${boundary}--`,
|
||||
''
|
||||
].join('\r\n');
|
||||
const request = {
|
||||
method: 'POST',
|
||||
url: 'https://api.example/send',
|
||||
headers: { 'content-type': `multipart/mixed; boundary=${boundary}` },
|
||||
data: rawMultipartBody
|
||||
};
|
||||
const result = interpolateVars(request, {} /* no missingVar */, null, null);
|
||||
expect(result.data).toContain('Token: ');
|
||||
});
|
||||
|
||||
it('interpolates multiple variables in a single line or JSON object', () => {
|
||||
const boundary = 'B2';
|
||||
const rawMultipartBody = [
|
||||
`--${boundary}`,
|
||||
'Content-Type: application/json',
|
||||
'',
|
||||
'{"id": "{{id}}", "msg": "{{msg}}", "extra": "{{extra}}"}',
|
||||
`--${boundary}--`,
|
||||
''
|
||||
].join('\r\n');
|
||||
const request = {
|
||||
method: 'POST',
|
||||
url: 'https://api.example/send',
|
||||
headers: { 'content-type': `multipart/mixed; boundary=${boundary}` },
|
||||
data: rawMultipartBody
|
||||
};
|
||||
const result = interpolateVars(request, { id: 1, msg: 'hi', extra: 'x' }, null, null);
|
||||
expect(result.data).toContain('"id": "1", "msg": "hi", "extra": "x"');
|
||||
});
|
||||
|
||||
it('interpolates variables inside quoted and unquoted contexts', () => {
|
||||
const boundary = 'B3';
|
||||
const rawMultipartBody = [
|
||||
`--${boundary}`,
|
||||
'Content-Disposition: form-data; name="{{fieldName}}"',
|
||||
'',
|
||||
'Value',
|
||||
`--${boundary}--`,
|
||||
''
|
||||
].join('\r\n');
|
||||
const request = {
|
||||
method: 'POST',
|
||||
url: 'https://api.example/send',
|
||||
headers: { 'content-type': `multipart/mixed; boundary=${boundary}` },
|
||||
data: rawMultipartBody
|
||||
};
|
||||
const result = interpolateVars(request, { fieldName: 'theField' }, null, null);
|
||||
expect(result.data).toContain('name="theField"');
|
||||
});
|
||||
|
||||
it('interpolates variables in both part headers and part bodies', () => {
|
||||
const boundary = 'B4';
|
||||
const rawMultipartBody = [
|
||||
`--${boundary}`,
|
||||
'Content-Type: text/plain; charset={{charset}}',
|
||||
'',
|
||||
'Token: {{token}}',
|
||||
`--${boundary}--`,
|
||||
''
|
||||
].join('\r\n');
|
||||
const request = {
|
||||
method: 'POST',
|
||||
url: 'https://api.example/send',
|
||||
headers: { 'content-type': `multipart/mixed; boundary=${boundary}` },
|
||||
data: rawMultipartBody
|
||||
};
|
||||
const result = interpolateVars(request, { charset: 'utf-8', token: 'abc' }, null, null);
|
||||
expect(result.data).toContain('charset=utf-8');
|
||||
expect(result.data).toContain('Token: abc');
|
||||
});
|
||||
|
||||
it('interpolates variables in the final boundary line', () => {
|
||||
const boundary = 'B5';
|
||||
const rawMultipartBody = [
|
||||
`--${boundary}`,
|
||||
'Content-Type: text/plain',
|
||||
'',
|
||||
'End',
|
||||
`--{{finalBoundary}}--`,
|
||||
''
|
||||
].join('\r\n');
|
||||
const request = {
|
||||
method: 'POST',
|
||||
url: 'https://api.example/send',
|
||||
headers: { 'content-type': `multipart/mixed; boundary=${boundary}` },
|
||||
data: rawMultipartBody
|
||||
};
|
||||
const result = interpolateVars(request, { finalBoundary: boundary }, null, null);
|
||||
expect(result.data).toContain(`--${boundary}--`);
|
||||
});
|
||||
|
||||
it('interpolates variables that appear multiple times in the body', () => {
|
||||
const boundary = 'B6';
|
||||
const rawMultipartBody = [
|
||||
`--${boundary}`,
|
||||
'Content-Type: text/plain',
|
||||
'',
|
||||
'Token: {{token}}, Again: {{token}}',
|
||||
`--${boundary}--`,
|
||||
''
|
||||
].join('\r\n');
|
||||
const request = {
|
||||
method: 'POST',
|
||||
url: 'https://api.example/send',
|
||||
headers: { 'content-type': `multipart/mixed; boundary=${boundary}` },
|
||||
data: rawMultipartBody
|
||||
};
|
||||
const result = interpolateVars(request, { token: 'repeat' }, null, null);
|
||||
expect(result.data.match(/repeat/g).length).toBe(2);
|
||||
});
|
||||
|
||||
it('leaves body unchanged if no variables present', () => {
|
||||
const boundary = 'B7';
|
||||
const rawMultipartBody = [
|
||||
`--${boundary}`,
|
||||
'Content-Type: text/plain',
|
||||
'',
|
||||
'No variables here',
|
||||
`--${boundary}--`,
|
||||
''
|
||||
].join('\r\n');
|
||||
const request = {
|
||||
method: 'POST',
|
||||
url: 'https://api.example/send',
|
||||
headers: { 'content-type': `multipart/mixed; boundary=${boundary}` },
|
||||
data: rawMultipartBody
|
||||
};
|
||||
const result = interpolateVars(request, {}, null, null);
|
||||
expect(result.data).toBe(rawMultipartBody);
|
||||
});
|
||||
});
|
||||
|
||||
describe('File body streaming', () => {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
meta {
|
||||
name: content-types-mixed-interpolation
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{echo-host}}
|
||||
body: text
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:text {
|
||||
------MyCustomBoundaryString
|
||||
Content-Disposition: form-data; name="metadata"
|
||||
Content-Type: application/json
|
||||
|
||||
{{version}}
|
||||
|
||||
------MyCustomBoundaryString--
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
version: 0.0.1
|
||||
}
|
||||
|
||||
assert {
|
||||
res.body: contains 0.0.1
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
meta {
|
||||
name: multipart-mixed-form-data-file
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{echo-host}}
|
||||
body: multipartForm
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:multipart-form {
|
||||
sample: @file(bruno.png) @contentType(image/png)
|
||||
}
|
||||
|
||||
assert {
|
||||
res.body: matches ^[-]+[a-z0-9]+
|
||||
res.body: contains Content-Type: image/png
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
meta {
|
||||
name: multipart-mixed-form-data-parse
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{echo-host}}
|
||||
body: multipartForm
|
||||
auth: none
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: multipart/mixed
|
||||
}
|
||||
|
||||
body:multipart-form {
|
||||
sample: sample
|
||||
}
|
||||
|
||||
assert {
|
||||
res.body: matches ^[-]+[a-z0-9]+
|
||||
}
|
||||
Reference in New Issue
Block a user