mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-09 06:55:03 +00:00
fix: allow user to delete default bruno headers in pre-request (#7331)
* fix: allow user to delete default bruno headers * fix: resolved comments --------- Co-authored-by: shubh-bruno <shubh-bruno@shubh-bruno.local>
This commit is contained in:
@@ -78,14 +78,35 @@ function makeAxiosInstance({ requestMaxRedirects = 5, disableCookies, followRedi
|
|||||||
const instance = axios.create({
|
const instance = axios.create({
|
||||||
proxy: false,
|
proxy: false,
|
||||||
maxRedirects: 0,
|
maxRedirects: 0,
|
||||||
headers: {
|
headers: {}
|
||||||
'User-Agent': `bruno-runtime/${CLI_VERSION}`
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set User-Agent manually (using transformRequest to delete headers instead)
|
||||||
|
instance.defaults.headers.common = {
|
||||||
|
'User-Agent': `bruno-runtime/${CLI_VERSION}`
|
||||||
|
};
|
||||||
|
|
||||||
instance.interceptors.request.use((config) => {
|
instance.interceptors.request.use((config) => {
|
||||||
config.headers['request-start-time'] = Date.now();
|
config.headers['request-start-time'] = Date.now();
|
||||||
|
|
||||||
|
/**
|
||||||
|
Apply header deletions requested via req.deleteHeader() in pre-request scripts.
|
||||||
|
Using set(name, null) rather than delete(): the axios http adapter guards its
|
||||||
|
own defaults (User-Agent, Accept-Encoding) with set(..., false) which only
|
||||||
|
skips writing when the key already exists. delete() removes the key entirely,
|
||||||
|
so the guard misses and the adapter re-adds the default. null keeps the key
|
||||||
|
present (blocking the guard) while toJSON() omits null values from the wire.
|
||||||
|
*/
|
||||||
|
const headersToDelete = config.__headersToDelete;
|
||||||
|
if (headersToDelete && Array.isArray(headersToDelete)) {
|
||||||
|
headersToDelete.forEach((headerName) => {
|
||||||
|
const lower = headerName.toLowerCase();
|
||||||
|
if (lower === 'host' || lower === 'connection') return;
|
||||||
|
config.headers.set(headerName, null);
|
||||||
|
});
|
||||||
|
delete config.__headersToDelete;
|
||||||
|
}
|
||||||
|
|
||||||
// Add cookies to request if available and not disabled
|
// Add cookies to request if available and not disabled
|
||||||
if (!disableCookies) {
|
if (!disableCookies) {
|
||||||
const cookieString = getCookieStringForUrl(config.url);
|
const cookieString = getCookieStringForUrl(config.url);
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ function makeAxiosInstance({
|
|||||||
} = {}) {
|
} = {}) {
|
||||||
/** @type {axios.AxiosInstance} */
|
/** @type {axios.AxiosInstance} */
|
||||||
const instance = axios.create({
|
const instance = axios.create({
|
||||||
transformRequest: function transformRequest(data, headers) {
|
transformRequest: function (data, headers) {
|
||||||
const contentType = headers?.['Content-Type'] || headers?.['content-type'] || '';
|
const contentType = headers?.['Content-Type'] || headers?.['content-type'] || '';
|
||||||
const hasJSONContentType = contentType.includes('json');
|
const hasJSONContentType = contentType.includes('json');
|
||||||
if (typeof data === 'string' && hasJSONContentType) {
|
if (typeof data === 'string' && hasJSONContentType) {
|
||||||
@@ -95,11 +95,14 @@ function makeAxiosInstance({
|
|||||||
},
|
},
|
||||||
proxy: false,
|
proxy: false,
|
||||||
maxRedirects: 0,
|
maxRedirects: 0,
|
||||||
headers: {
|
headers: {}
|
||||||
'User-Agent': `bruno-runtime/${version}`
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set User-Agent manually (using transformRequest to delete headers instead)
|
||||||
|
instance.defaults.headers.common = {
|
||||||
|
'User-Agent': `bruno-runtime/${version}`
|
||||||
|
};
|
||||||
|
|
||||||
instance.interceptors.request.use(async (config) => {
|
instance.interceptors.request.use(async (config) => {
|
||||||
const url = URL.parse(config.url);
|
const url = URL.parse(config.url);
|
||||||
config.metadata = config.metadata || {};
|
config.metadata = config.metadata || {};
|
||||||
@@ -121,29 +124,13 @@ function makeAxiosInstance({
|
|||||||
message: `Current time is ${new Date().toISOString()}`
|
message: `Current time is ${new Date().toISOString()}`
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add request method and headers
|
// Add request method line
|
||||||
timeline.push({
|
timeline.push({
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
type: 'request',
|
type: 'request',
|
||||||
message: `${config.method.toUpperCase()} ${config.url}`
|
message: `${config.method.toUpperCase()} ${config.url}`
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.entries(config.headers).forEach(([key, value]) => {
|
|
||||||
// See https://github.com/usebruno/bruno/issues/1693
|
|
||||||
// Axios adds 'Content-Type': 'application/x-www-form-urlencoded for requests with no body
|
|
||||||
// Bruno sets content-type: false for no body requests so that axios doesn't add the default content-type header
|
|
||||||
// Hence we skip content-type if it's false
|
|
||||||
if (key.toLowerCase() === 'content-type' && value === false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
timeline.push({
|
|
||||||
timestamp: new Date(),
|
|
||||||
type: 'requestHeader',
|
|
||||||
message: `${key}: ${value}`
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add request data if available
|
// Add request data if available
|
||||||
if (config.data) {
|
if (config.data) {
|
||||||
let requestData = typeof config.data === 'string' ? config.data : JSON.stringify(config.data, null, 2);
|
let requestData = typeof config.data === 'string' ? config.data : JSON.stringify(config.data, null, 2);
|
||||||
@@ -170,6 +157,43 @@ function makeAxiosInstance({
|
|||||||
|
|
||||||
config.headers['request-start-time'] = Date.now();
|
config.headers['request-start-time'] = Date.now();
|
||||||
|
|
||||||
|
/**
|
||||||
|
Apply header deletions requested via req.deleteHeader() in pre-request scripts.
|
||||||
|
Using set(name, null) rather than delete(): the axios http adapter guards its
|
||||||
|
own defaults (User-Agent, Accept-Encoding) with set(..., false) which only
|
||||||
|
skips writing when the key already exists. delete() removes the key entirely,
|
||||||
|
so the guard misses and the adapter re-adds the default. null keeps the key
|
||||||
|
present (blocking the guard) while toJSON() omits null values from the wire.
|
||||||
|
*/
|
||||||
|
const headersToDelete = config.__headersToDelete;
|
||||||
|
let deleteConnection = false;
|
||||||
|
|
||||||
|
if (headersToDelete && Array.isArray(headersToDelete)) {
|
||||||
|
headersToDelete.forEach((headerName) => {
|
||||||
|
const lower = headerName.toLowerCase();
|
||||||
|
if (lower === 'host') return;
|
||||||
|
if (lower === 'connection') {
|
||||||
|
// Handled after setupProxyAgents to avoid being overwritten by keepAlive:true.
|
||||||
|
deleteConnection = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
config.headers.set(headerName, null);
|
||||||
|
});
|
||||||
|
delete config.__headersToDelete;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log request headers AFTER deletion so the timeline reflects what is actually sent.
|
||||||
|
// Skip null values (headers marked for deletion) and false values (e.g. content-type
|
||||||
|
// suppressed for no-body requests — see https://github.com/usebruno/bruno/issues/1693).
|
||||||
|
Object.entries(config.headers).forEach(([key, value]) => {
|
||||||
|
if (value === null || value === false) return;
|
||||||
|
timeline.push({
|
||||||
|
timestamp: new Date(),
|
||||||
|
type: 'requestHeader',
|
||||||
|
message: `${key}: ${value}`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const agentOptions = {
|
const agentOptions = {
|
||||||
...httpsAgentRequestFields,
|
...httpsAgentRequestFields,
|
||||||
keepAlive: true
|
keepAlive: true
|
||||||
@@ -195,6 +219,7 @@ function makeAxiosInstance({
|
|||||||
message: `Error setting up proxy agents: ${err?.message}`
|
message: `Error setting up proxy agents: ${err?.message}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
config.metadata.timeline = timeline;
|
config.metadata.timeline = timeline;
|
||||||
return config;
|
return config;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -126,10 +126,7 @@ class BrunoRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deleteHeaders(headers) {
|
deleteHeaders(headers) {
|
||||||
headers.forEach((name) => {
|
headers.forEach((name) => this.deleteHeader(name));
|
||||||
delete this.headers[name];
|
|
||||||
delete this.req.headers[name];
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getHeader(name) {
|
getHeader(name) {
|
||||||
@@ -144,6 +141,18 @@ class BrunoRequest {
|
|||||||
deleteHeader(name) {
|
deleteHeader(name) {
|
||||||
delete this.headers[name];
|
delete this.headers[name];
|
||||||
delete this.req.headers[name];
|
delete this.req.headers[name];
|
||||||
|
|
||||||
|
/**
|
||||||
|
Store header name to be applied in the axios request interceptor.
|
||||||
|
Default headers (user-agent, accept, accept-encoding, etc.) are added after
|
||||||
|
the pre-request script runs, so we track them here and delete them later.
|
||||||
|
*/
|
||||||
|
if (!this.req.__headersToDelete) {
|
||||||
|
this.req.__headersToDelete = [];
|
||||||
|
}
|
||||||
|
if (!this.req.__headersToDelete.includes(name)) {
|
||||||
|
this.req.__headersToDelete.push(name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hasJSONContentType(headers) {
|
hasJSONContentType(headers) {
|
||||||
|
|||||||
116
packages/bruno-js/tests/bruno-request-delete-header.spec.js
Normal file
116
packages/bruno-js/tests/bruno-request-delete-header.spec.js
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
const { describe, it, expect, beforeEach } = require('@jest/globals');
|
||||||
|
const BrunoRequest = require('../src/bruno-request');
|
||||||
|
|
||||||
|
const makeReq = (overrides = {}) => ({
|
||||||
|
url: 'http://localhost:5000/api',
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...overrides.headers
|
||||||
|
},
|
||||||
|
data: undefined,
|
||||||
|
...overrides
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('BrunoRequest - header deletion', () => {
|
||||||
|
describe('deleteHeader()', () => {
|
||||||
|
it('removes a user-set header from req.headers', () => {
|
||||||
|
const rawReq = makeReq({ headers: { 'X-Custom': 'value' } });
|
||||||
|
const req = new BrunoRequest(rawReq);
|
||||||
|
|
||||||
|
req.deleteHeader('X-Custom');
|
||||||
|
|
||||||
|
expect(rawReq.headers['X-Custom']).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds the header name to __headersToDelete on the req object', () => {
|
||||||
|
const rawReq = makeReq();
|
||||||
|
const req = new BrunoRequest(rawReq);
|
||||||
|
|
||||||
|
req.deleteHeader('user-agent');
|
||||||
|
|
||||||
|
expect(rawReq.__headersToDelete).toEqual(['user-agent']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tracks multiple deleteHeader calls without duplicates', () => {
|
||||||
|
const rawReq = makeReq();
|
||||||
|
const req = new BrunoRequest(rawReq);
|
||||||
|
|
||||||
|
req.deleteHeader('user-agent');
|
||||||
|
req.deleteHeader('accept');
|
||||||
|
req.deleteHeader('user-agent'); // duplicate – should not be added again
|
||||||
|
|
||||||
|
expect(rawReq.__headersToDelete).toEqual(['user-agent', 'accept']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does NOT attach a non-enumerable __headersToDelete to req.headers', () => {
|
||||||
|
const rawReq = makeReq();
|
||||||
|
const req = new BrunoRequest(rawReq);
|
||||||
|
|
||||||
|
req.deleteHeader('accept-encoding');
|
||||||
|
|
||||||
|
// The non-enumerable approach was removed; __headersToDelete must NOT be on headers
|
||||||
|
expect(rawReq.headers.__headersToDelete).toBeUndefined();
|
||||||
|
// But it must be on the req config object itself
|
||||||
|
expect(rawReq.__headersToDelete).toContain('accept-encoding');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteHeaders()', () => {
|
||||||
|
it('removes multiple user-set headers from req.headers', () => {
|
||||||
|
const rawReq = makeReq({ headers: { 'X-A': '1', 'X-B': '2', 'X-C': '3' } });
|
||||||
|
const req = new BrunoRequest(rawReq);
|
||||||
|
|
||||||
|
req.deleteHeaders(['X-A', 'X-C']);
|
||||||
|
|
||||||
|
expect(rawReq.headers['X-A']).toBeUndefined();
|
||||||
|
expect(rawReq.headers['X-C']).toBeUndefined();
|
||||||
|
expect(rawReq.headers['X-B']).toBe('2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds all names to __headersToDelete so default headers can be suppressed', () => {
|
||||||
|
const rawReq = makeReq();
|
||||||
|
const req = new BrunoRequest(rawReq);
|
||||||
|
|
||||||
|
req.deleteHeaders(['user-agent', 'accept', 'accept-encoding']);
|
||||||
|
|
||||||
|
expect(rawReq.__headersToDelete).toEqual(['user-agent', 'accept', 'accept-encoding']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not duplicate entries when deleteHeaders is called with the same name twice', () => {
|
||||||
|
const rawReq = makeReq();
|
||||||
|
const req = new BrunoRequest(rawReq);
|
||||||
|
|
||||||
|
req.deleteHeaders(['user-agent', 'accept']);
|
||||||
|
req.deleteHeaders(['user-agent']); // duplicate
|
||||||
|
|
||||||
|
expect(rawReq.__headersToDelete).toEqual(['user-agent', 'accept']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('delegates to deleteHeader so tracking is consistent', () => {
|
||||||
|
const rawReq = makeReq({ headers: { 'X-Test': 'hello' } });
|
||||||
|
const req = new BrunoRequest(rawReq);
|
||||||
|
|
||||||
|
req.deleteHeaders(['X-Test', 'connection']);
|
||||||
|
|
||||||
|
// User-set header removed immediately
|
||||||
|
expect(rawReq.headers['X-Test']).toBeUndefined();
|
||||||
|
// Both tracked for interceptor
|
||||||
|
expect(rawReq.__headersToDelete).toContain('X-Test');
|
||||||
|
expect(rawReq.__headersToDelete).toContain('connection');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('host header protection', () => {
|
||||||
|
it('still tracks host in __headersToDelete even though the interceptor will ignore it', () => {
|
||||||
|
// The protection lives in the axios interceptor, not in BrunoRequest itself.
|
||||||
|
// BrunoRequest just tracks whatever the user asks to delete.
|
||||||
|
const rawReq = makeReq();
|
||||||
|
const req = new BrunoRequest(rawReq);
|
||||||
|
|
||||||
|
req.deleteHeader('host');
|
||||||
|
|
||||||
|
expect(rawReq.__headersToDelete).toContain('host');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -20,6 +20,7 @@ import https from 'node:https';
|
|||||||
|
|
||||||
type ModifiedInternalAxiosRequestConfig = InternalAxiosRequestConfig & {
|
type ModifiedInternalAxiosRequestConfig = InternalAxiosRequestConfig & {
|
||||||
startTime: number;
|
startTime: number;
|
||||||
|
__headersToDelete?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type ModifiedAxiosResponse = AxiosResponse & {
|
type ModifiedAxiosResponse = AxiosResponse & {
|
||||||
@@ -51,10 +52,28 @@ const makeAxiosInstance = (customRequestConfig?: AxiosRequestConfig) => {
|
|||||||
customRequestConfig = customRequestConfig || {};
|
customRequestConfig = customRequestConfig || {};
|
||||||
const axiosInstance = axios.create({
|
const axiosInstance = axios.create({
|
||||||
...baseRequestConfig,
|
...baseRequestConfig,
|
||||||
...customRequestConfig
|
...customRequestConfig,
|
||||||
|
headers: {}
|
||||||
});
|
});
|
||||||
|
|
||||||
axiosInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
axiosInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
||||||
|
// Apply header deletions requested via req.deleteHeader() in pre-request scripts.
|
||||||
|
const modConfig = config as ModifiedInternalAxiosRequestConfig;
|
||||||
|
const headersToDelete = modConfig.__headersToDelete;
|
||||||
|
if (headersToDelete && Array.isArray(headersToDelete)) {
|
||||||
|
headersToDelete.forEach((headerName: string) => {
|
||||||
|
const lower = headerName.toLowerCase();
|
||||||
|
if (lower === 'host' || lower === 'connection') return;
|
||||||
|
// Using set(name, null) rather than delete(): the axios http adapter guards its
|
||||||
|
// own defaults (User-Agent, Accept-Encoding) with set(..., false) which only
|
||||||
|
// skips writing when the key already exists. delete() removes the key entirely,
|
||||||
|
// so the guard misses and the adapter re-adds the default. null keeps the key
|
||||||
|
// present (blocking the guard) while toJSON() omits null values from the wire.
|
||||||
|
config.headers.set(headerName, null);
|
||||||
|
});
|
||||||
|
delete modConfig.__headersToDelete;
|
||||||
|
}
|
||||||
|
|
||||||
const modifiedConfig: ModifiedInternalAxiosRequestConfig = {
|
const modifiedConfig: ModifiedInternalAxiosRequestConfig = {
|
||||||
...config,
|
...config,
|
||||||
startTime: Date.now()
|
startTime: Date.now()
|
||||||
|
|||||||
Reference in New Issue
Block a user