fix: preserve user-defined boundary in multipart/mixed Content-Type header (#7531)

* fix: preserve user-defined boundary in multipart/mixed Content-Type header

When users specify a boundary parameter in their Content-Type header for
multipart/mixed requests with TEXT body mode, Bruno now preserves the
user-defined boundary instead of generating a new one.

Fixes: https://github.com/usebruno/bruno/issues/7523

* updated the test to use local server and changed the request method to GET

* fix: handle quoted boundary values in Content-Type header extraction

---------

Co-authored-by: Chirag Chandrashekhar <cchirag85@gmail.com>
This commit is contained in:
Chirag Chandrashekhar
2026-03-27 16:49:50 +05:30
committed by GitHub
parent 53b75d083f
commit bbf3cb8dd3
7 changed files with 264 additions and 6 deletions

View File

@@ -26,7 +26,7 @@ const { addDigestInterceptor, getHttpHttpsAgents, makeAxiosInstance: makeAxiosIn
const { getCACertificates, transformProxyConfig, getOrCreateHttpsAgent, getOrCreateHttpAgent } = require('@usebruno/requests');
const { getOAuth2Token, getFormattedOauth2Credentials } = require('../utils/oauth2');
const tokenStore = require('../store/tokenStore');
const { encodeUrl, buildFormUrlEncodedPayload, extractPromptVariables, isFormData } = require('@usebruno/common').utils;
const { encodeUrl, buildFormUrlEncodedPayload, extractPromptVariables, isFormData, extractBoundaryFromContentType } = require('@usebruno/common').utils;
const onConsoleLog = (type, args) => {
console[type](...args);
@@ -570,7 +570,12 @@ const runSingleRequest = async function (
if (contentType !== 'multipart/form-data') {
// Patch: Axios leverages getHeaders method to get the headers so FormData should be monkey patched
const formHeaders = form.getHeaders();
formHeaders['content-type'] = `${contentType}; boundary=${form.getBoundary()}`;
const existingBoundary = extractBoundaryFromContentType(contentType);
if (existingBoundary) {
formHeaders['content-type'] = contentType;
} else {
formHeaders['content-type'] = `${contentType}; boundary=${form.getBoundary()}`;
}
form.getHeaders = function () {
return formHeaders;
};

View File

@@ -1,5 +1,5 @@
import { describe, it, expect } from '@jest/globals';
import { buildFormUrlEncodedPayload, isFormData } from './form-data';
import { buildFormUrlEncodedPayload, isFormData, extractBoundaryFromContentType } from './form-data';
import FormData from 'form-data';
describe('buildFormUrlEncodedPayload', () => {
@@ -161,3 +161,51 @@ describe('isFormData', () => {
expect(isFormData(formData)).toBe(true);
});
});
describe('extractBoundaryFromContentType', () => {
it('should extract boundary from Content-Type header', () => {
expect(extractBoundaryFromContentType('multipart/mixed; boundary=my-boundary')).toBe('my-boundary');
});
it('should extract boundary with dashes', () => {
expect(extractBoundaryFromContentType('multipart/mixed; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW')).toBe('----WebKitFormBoundary7MA4YWxkTrZu0gW');
});
it('should extract boundary case-insensitively', () => {
expect(extractBoundaryFromContentType('multipart/mixed; BOUNDARY=my-boundary')).toBe('my-boundary');
expect(extractBoundaryFromContentType('multipart/mixed; Boundary=my-boundary')).toBe('my-boundary');
});
it('should extract boundary when other params exist', () => {
expect(extractBoundaryFromContentType('multipart/mixed; charset=utf-8; boundary=my-boundary')).toBe('my-boundary');
expect(extractBoundaryFromContentType('multipart/mixed; boundary=my-boundary; charset=utf-8')).toBe('my-boundary');
});
it('should return null when no boundary exists', () => {
expect(extractBoundaryFromContentType('multipart/mixed')).toBeNull();
expect(extractBoundaryFromContentType('application/json')).toBeNull();
});
it('should return null for non-string input', () => {
expect(extractBoundaryFromContentType(null)).toBeNull();
expect(extractBoundaryFromContentType(undefined)).toBeNull();
expect(extractBoundaryFromContentType(123)).toBeNull();
expect(extractBoundaryFromContentType({})).toBeNull();
});
it('should handle empty string', () => {
expect(extractBoundaryFromContentType('')).toBeNull();
});
it('should extract boundary from quoted value', () => {
expect(extractBoundaryFromContentType('multipart/mixed; boundary="my-boundary"')).toBe('my-boundary');
});
it('should extract quoted boundary with spaces', () => {
expect(extractBoundaryFromContentType('multipart/mixed; boundary="my boundary value"')).toBe('my boundary value');
});
it('should extract quoted boundary when other params exist', () => {
expect(extractBoundaryFromContentType('multipart/mixed; charset=utf-8; boundary="my-boundary"')).toBe('my-boundary');
});
});

View File

@@ -43,3 +43,16 @@ export const isFormData = (obj: unknown): boolean => {
// todo: checking constructor.name can produce false positives for objects that have a constructor.name property set to 'FormData', but this is rare.
return obj?.constructor?.name === 'FormData';
};
/**
* Extracts boundary parameter from a Content-Type header value.
* @param contentType - The Content-Type header value (e.g., "multipart/mixed; boundary=my-boundary")
* @returns The boundary value if found, or null if not present
*/
export const extractBoundaryFromContentType = (contentType: unknown): string | null => {
if (typeof contentType !== 'string') {
return null;
}
const match = contentType.match(/boundary="([^"]+)"|boundary=([^;\s]+)/i);
return match ? (match[1] || match[2]) : null;
};

View File

@@ -7,7 +7,8 @@ export {
export {
buildFormUrlEncodedPayload,
isFormData
isFormData,
extractBoundaryFromContentType
} from './form-data';
export {

View File

@@ -35,7 +35,7 @@ const { cookiesStore } = require('../../store/cookies');
const registerGrpcEventHandlers = require('./grpc-event-handlers');
const { registerWsEventHandlers } = require('./ws-event-handlers');
const { getCertsAndProxyConfig, buildCertsAndProxyConfig } = require('./cert-utils');
const { buildFormUrlEncodedPayload, isFormData } = require('@usebruno/common').utils;
const { buildFormUrlEncodedPayload, isFormData, extractBoundaryFromContentType } = require('@usebruno/common').utils;
const ERROR_OCCURRED_WHILE_EXECUTING_REQUEST = 'Error occurred while executing the request!';
@@ -609,7 +609,12 @@ const registerNetworkIpc = (mainWindow) => {
if (contentType !== 'multipart/form-data') {
// Patch: Axios leverages getHeaders method to get the headers so FormData should be monkey patched
const formHeaders = form.getHeaders();
formHeaders['content-type'] = `${contentType}; boundary=${form.getBoundary()}`;
const existingBoundary = extractBoundaryFromContentType(contentType);
if (existingBoundary) {
formHeaders['content-type'] = contentType;
} else {
formHeaders['content-type'] = `${contentType}; boundary=${form.getBoundary()}`;
}
form.getHeaders = function () {
return formHeaders;
};

View File

@@ -5,6 +5,15 @@ router.get('/path/*', (req, res) => {
return res.json({ url: req.url });
});
// Echo back request headers - useful for testing header manipulation
router.all('/headers', (req, res) => {
return res.json({
method: req.method,
headers: req.headers,
url: req.url
});
});
router.post('/json', (req, res) => {
return res.json(req.body);
});