mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-09 06:55:03 +00:00
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:
committed by
GitHub
parent
53b75d083f
commit
bbf3cb8dd3
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -7,7 +7,8 @@ export {
|
||||
|
||||
export {
|
||||
buildFormUrlEncodedPayload,
|
||||
isFormData
|
||||
isFormData,
|
||||
extractBoundaryFromContentType
|
||||
} from './form-data';
|
||||
|
||||
export {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user