diff --git a/packages/bruno-cli/src/utils/axios-instance.js b/packages/bruno-cli/src/utils/axios-instance.js index cf94f8536..d557639c7 100644 --- a/packages/bruno-cli/src/utils/axios-instance.js +++ b/packages/bruno-cli/src/utils/axios-instance.js @@ -92,10 +92,11 @@ function makeAxiosInstance({ headers: {} }); - // Set User-Agent manually (using transformRequest to delete headers instead) - instance.defaults.headers.common = { - 'User-Agent': `bruno-runtime/${CLI_VERSION}` - }; + // Extend common headers with User-Agent rather than replacing the object. + // axios.create() preserves defaults.headers.common = { Accept: 'application/json, text/plain, */*' }. + // Assigning a new object (= { 'User-Agent': ... }) would nuke that default, causing servers that + // rely on content-negotiation to receive requests with no Accept header. + instance.defaults.headers.common['User-Agent'] = `bruno-runtime/${CLI_VERSION}`; instance.interceptors.request.use((config) => { config.headers['request-start-time'] = Date.now(); diff --git a/packages/bruno-cli/tests/utils/axios-instance.spec.js b/packages/bruno-cli/tests/utils/axios-instance.spec.js new file mode 100644 index 000000000..1e25f317f --- /dev/null +++ b/packages/bruno-cli/tests/utils/axios-instance.spec.js @@ -0,0 +1,37 @@ +const { describe, it, expect } = require('@jest/globals'); +const { makeAxiosInstance } = require('../../src/utils/axios-instance'); + +function createStubAdapter() { + let capturedConfig = null; + + const adapter = (config) => { + capturedConfig = config; + return Promise.resolve({ data: {}, status: 200, statusText: 'OK', headers: {}, config }); + }; + + adapter.getConfig = () => capturedConfig; + + return adapter; +} + +describe('makeAxiosInstance', () => { + it('setting User-Agent does not clobber the axios default Accept header', async () => { + const stubAdapter = createStubAdapter(); + const instance = makeAxiosInstance(); + + await instance({ url: 'https://api.example.com/test', method: 'get', adapter: stubAdapter }); + + // axios.create() sets Accept by default; assigning a new object to defaults.headers.common + // would nuke it. Guard against that regression. + expect(stubAdapter.getConfig().headers['Accept']).toMatch(/application\/json/); + }); + + it('sets User-Agent header to bruno-runtime version', async () => { + const stubAdapter = createStubAdapter(); + const instance = makeAxiosInstance(); + + await instance({ url: 'https://api.example.com/test', method: 'get', adapter: stubAdapter }); + + expect(stubAdapter.getConfig().headers['User-Agent']).toMatch(/^bruno-runtime\//); + }); +}); diff --git a/packages/bruno-electron/src/ipc/network/axios-instance.js b/packages/bruno-electron/src/ipc/network/axios-instance.js index a22475bc8..19b481190 100644 --- a/packages/bruno-electron/src/ipc/network/axios-instance.js +++ b/packages/bruno-electron/src/ipc/network/axios-instance.js @@ -99,10 +99,11 @@ function makeAxiosInstance({ headers: {} }); - // Set User-Agent manually (using transformRequest to delete headers instead) - instance.defaults.headers.common = { - 'User-Agent': `bruno-runtime/${version}` - }; + // Extend common headers with User-Agent rather than replacing the object. + // axios.create() preserves defaults.headers.common = { Accept: 'application/json, text/plain, */*' }. + // Assigning a new object (= { 'User-Agent': ... }) would nuke that default, causing servers that + // rely on content-negotiation to receive requests with no Accept header. + instance.defaults.headers.common['User-Agent'] = `bruno-runtime/${version}`; instance.interceptors.request.use(async (config) => { const url = URL.parse(config.url); diff --git a/packages/bruno-electron/tests/network/axios-instance.spec.js b/packages/bruno-electron/tests/network/axios-instance.spec.js index 7ace03d66..407ff102c 100644 --- a/packages/bruno-electron/tests/network/axios-instance.spec.js +++ b/packages/bruno-electron/tests/network/axios-instance.spec.js @@ -51,6 +51,28 @@ function createStubAdapter() { return adapter; } +describe('axios-instance: default headers', () => { + test('setting User-Agent does not clobber the axios default Accept header', async () => { + const stubAdapter = createStubAdapter(); + const instance = makeAxiosInstance(); + + await instance({ url: 'https://api.example.com/test', method: 'get', adapter: stubAdapter }); + + // axios.create() sets Accept by default; assigning a new object to defaults.headers.common + // would nuke it. Guard against that regression. + expect(stubAdapter.getConfig().headers['Accept']).toMatch(/application\/json/); + }); + + test('sets User-Agent header to bruno-runtime version', async () => { + const stubAdapter = createStubAdapter(); + const instance = makeAxiosInstance(); + + await instance({ url: 'https://api.example.com/test', method: 'get', adapter: stubAdapter }); + + expect(stubAdapter.getConfig().headers['User-Agent']).toMatch(/^bruno-runtime\//); + }); +}); + describe('axios-instance: DNS lookup behavior (GitHub #7343)', () => { let axiosInstance; diff --git a/packages/bruno-tests/collection/echo/echo default request headers.bru b/packages/bruno-tests/collection/echo/echo default request headers.bru new file mode 100644 index 000000000..b51b44d7f --- /dev/null +++ b/packages/bruno-tests/collection/echo/echo default request headers.bru @@ -0,0 +1,28 @@ +meta { + name: echo default request headers + type: http + seq: 14 +} + +post { + url: {{echo-host}} + body: none + auth: none +} + +tests { + test("sends Accept header with application/json by default", function() { + // The echo server reflects request headers back as response headers. + // Verifies that axios's default Accept header is not clobbered when + // setting User-Agent (regression for: defaults.headers.common object replacement). + const accept = res.getHeaders()["accept"]; + expect(accept).to.be.a("string"); + expect(accept).to.include("application/json"); + }); + + test("sends User-Agent header with bruno-runtime prefix", function() { + const userAgent = res.getHeaders()["user-agent"]; + expect(userAgent).to.be.a("string"); + expect(userAgent).to.match(/^bruno-runtime\//); + }); +}