From bafb235e729f2019122a0e296cfac8075c157c53 Mon Sep 17 00:00:00 2001 From: lohit Date: Mon, 2 Feb 2026 12:29:46 +0000 Subject: [PATCH] feat: add `certs and proxy` config to `bru.sendRequest` API (#6988) * feat: add certs and proxy config to bru.sendRequest API Co-Authored-By: Claude Opus 4.5 * fix: handle URL string argument in bru.sendRequest When bru.sendRequest is called with a plain URL string instead of a config object, the function now normalizes it to { url: string } before processing. This fixes the case where spreading a string created an invalid config object. Co-Authored-By: Claude Opus 4.5 * feat: add variable interpolation to bru.sendRequest certs and proxy config Interpolate environment variables in clientCertificates and proxy configuration for bru.sendRequest API, enabling use of variables like {{CERT_PATH}} or {{PROXY_HOST}} in certificate paths and proxy settings. Co-Authored-By: Claude Opus 4.5 * refactor: use interpolateObject for certs and proxy config interpolation - Add interpolateObject to electron's interpolate-string.js using buildCombinedVars pattern (matches CLI implementation) - Simplify cert-utils.js by using interpolateObject instead of manual field-by-field interpolation - Add interpolation for clientCertificates and proxy config in CLI's run-single-request.js for bru.sendRequest Co-Authored-By: Claude Opus 4.5 * refactor: add all variable types to sendRequest interpolation options - Add globalEnvVars, collectionVariables, folderVariables, requestVariables to sendRequestInterpolationOptions for complete variable support - Use cached system proxy instead of redundant getSystemProxy() call - Remove duplicate getOptions() call Co-Authored-By: Claude Opus 4.5 * refactor: skip CA cert loading when TLS verification is disabled Only load CA certificates when shouldVerifyTls is true, since they are not used for validation when TLS verification is disabled. Co-Authored-By: Claude Opus 4.5 --------- Co-authored-by: Claude Opus 4.5 --- .../src/utils/codemirror/autocomplete.js | 2 + .../src/runner/run-single-request.js | 32 ++- .../src/ipc/network/cert-utils.js | 68 +++++- .../bruno-electron/src/ipc/network/index.js | 30 ++- .../src/ipc/network/interpolate-string.js | 31 ++- packages/bruno-js/src/bru.js | 27 ++- .../bruno-js/src/runtime/assert-runtime.js | 4 +- .../bruno-js/src/runtime/script-runtime.js | 6 +- packages/bruno-js/src/runtime/test-runtime.js | 3 +- packages/bruno-js/src/runtime/vars-runtime.js | 3 +- .../bruno-requests/src/scripting/index.ts | 2 +- .../src/scripting/send-request.spec.ts | 198 ++++++++++++++++++ .../src/scripting/send-request.ts | 81 +++++-- .../src/utils/http-https-agents.ts | 24 ++- 14 files changed, 461 insertions(+), 50 deletions(-) create mode 100644 packages/bruno-requests/src/scripting/send-request.spec.ts diff --git a/packages/bruno-app/src/utils/codemirror/autocomplete.js b/packages/bruno-app/src/utils/codemirror/autocomplete.js index 6db1fd370..552820f6f 100644 --- a/packages/bruno-app/src/utils/codemirror/autocomplete.js +++ b/packages/bruno-app/src/utils/codemirror/autocomplete.js @@ -76,6 +76,8 @@ const STATIC_API_HINTS = { 'bru.setNextRequest(requestName)', 'bru.getRequestVar(key)', 'bru.runRequest(requestPathName)', + 'bru.sendRequest(requestConfig)', + 'bru.sendRequest(requestConfig, callback)', 'bru.getAssertionResults()', 'bru.getTestResults()', 'bru.sleep(ms)', diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index 701eec689..378bd4945 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -170,6 +170,37 @@ const runSingleRequest = async function ( const scriptingConfig = get(brunoConfig, 'scripts', {}); scriptingConfig.runtime = runtime; + // Build certsAndProxyConfig for bru.sendRequest + const options = getOptions(); + const systemProxyConfig = options['cachedSystemProxy']; + const sendRequestInterpolationOptions = { + envVars: envVariables, + runtimeVariables, + processEnvVars, + globalEnvVars, + collectionVariables: request.collectionVariables || {}, + folderVariables: request.folderVariables || {}, + requestVariables: request.requestVariables || {} + }; + const rawClientCertificates = get(brunoConfig, 'clientCertificates'); + const rawProxyConfig = get(brunoConfig, 'proxy', {}); + const certsAndProxyConfig = { + collectionPath, + options: { + noproxy: get(options, 'noproxy', false), + shouldVerifyTls: !get(options, 'insecure', false), + shouldUseCustomCaCertificate: !!options['cacert'], + customCaCertificateFilePath: options['cacert'], + shouldKeepDefaultCaCertificates: !options['ignoreTruststore'] + }, + clientCertificates: rawClientCertificates ? interpolateObject(rawClientCertificates, sendRequestInterpolationOptions) : undefined, + collectionLevelProxy: transformProxyConfig(interpolateObject(rawProxyConfig, sendRequestInterpolationOptions)), + systemProxyConfig + }; + + // Add certsAndProxyConfig to request object for bru.sendRequest + request.certsAndProxyConfig = certsAndProxyConfig; + // run pre request script const requestScriptFile = get(request, 'script.req'); const collectionName = collection?.brunoConfig?.name; @@ -237,7 +268,6 @@ const runSingleRequest = async function ( request.url = `http://${request.url}`; } - const options = getOptions(); const insecure = get(options, 'insecure', false); const noproxy = get(options, 'noproxy', false); const cachedSystemProxy = get(options, 'cachedSystemProxy', null); diff --git a/packages/bruno-electron/src/ipc/network/cert-utils.js b/packages/bruno-electron/src/ipc/network/cert-utils.js index 9fb752792..1c6d56046 100644 --- a/packages/bruno-electron/src/ipc/network/cert-utils.js +++ b/packages/bruno-electron/src/ipc/network/cert-utils.js @@ -5,7 +5,7 @@ const { getCACertificates } = require('@usebruno/requests'); const { preferencesUtil } = require('../../store/preferences'); const { getBrunoConfig } = require('../../store/bruno-config'); const { getCachedSystemProxy } = require('../../store/system-proxy'); -const { interpolateString } = require('./interpolate-string'); +const { interpolateString, interpolateObject } = require('./interpolate-string'); /** * Gets certificates and proxy configuration for a request @@ -155,4 +155,68 @@ const getCertsAndProxyConfig = async ({ return { proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions }; }; -module.exports = { getCertsAndProxyConfig }; +/** + * Builds the certsAndProxyConfig object for bru.sendRequest + * This allows bru.sendRequest to use the same proxy/certs config as the main request + */ +const buildCertsAndProxyConfig = async ({ + collectionUid, + collection, + collectionPath, + envVars, + runtimeVariables, + processEnvVars, + request +}) => { + const brunoConfig = getBrunoConfig(collectionUid, collection); + + // Build interpolation options (same pattern as getCertsAndProxyConfig) + const globalEnvironmentVariables = collection.globalEnvironmentVariables || {}; + const { promptVariables } = collection; + const collectionVariables = request?.collectionVariables || {}; + const folderVariables = request?.folderVariables || {}; + const requestVariables = request?.requestVariables || {}; + + const interpolationOptions = { + globalEnvironmentVariables, + collectionVariables, + envVars, + folderVariables, + requestVariables, + runtimeVariables, + promptVariables, + processEnvVars + }; + + // Build options for getHttpHttpsAgents + const options = { + noproxy: false, + shouldVerifyTls: preferencesUtil.shouldVerifyTls(), + shouldUseCustomCaCertificate: preferencesUtil.shouldUseCustomCaCertificate(), + customCaCertificateFilePath: preferencesUtil.getCustomCaCertificateFilePath(), + shouldKeepDefaultCaCertificates: preferencesUtil.shouldKeepDefaultCaCertificates() + }; + + // Get client certificates from bruno config and interpolate + const rawClientCertificates = get(brunoConfig, 'clientCertificates'); + const clientCertificates = rawClientCertificates + ? interpolateObject(rawClientCertificates, interpolationOptions) + : undefined; + + // Get proxy config from bruno config and interpolate + const collectionProxyConfig = get(brunoConfig, 'proxy', {}); + const collectionLevelProxy = interpolateObject(collectionProxyConfig, interpolationOptions); + + // Get system proxy config + const systemProxyConfig = getCachedSystemProxy(); + + return { + collectionPath, + options, + clientCertificates, + collectionLevelProxy, + systemProxyConfig + }; +}; + +module.exports = { getCertsAndProxyConfig, buildCertsAndProxyConfig }; diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 7aea48f39..ab5d8d3a9 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -34,7 +34,7 @@ const { isRequestTagsIncluded } = require('@usebruno/common'); const { cookiesStore } = require('../../store/cookies'); const registerGrpcEventHandlers = require('./grpc-event-handlers'); const { registerWsEventHandlers } = require('./ws-event-handlers'); -const { getCertsAndProxyConfig } = require('./cert-utils'); +const { getCertsAndProxyConfig, buildCertsAndProxyConfig } = require('./cert-utils'); const { buildFormUrlEncodedPayload, isFormData } = require('@usebruno/common').utils; const ERROR_OCCURRED_WHILE_EXECUTING_REQUEST = 'Error occurred while executing the request!'; @@ -680,6 +680,20 @@ const registerNetworkIpc = (mainWindow) => { request.signal = abortController.signal; saveCancelToken(cancelTokenUid, abortController); + // Build certsAndProxyConfig for bru.sendRequest + const certsAndProxyConfig = await buildCertsAndProxyConfig({ + collectionUid, + collection, + collectionPath, + envVars, + runtimeVariables, + processEnvVars, + request + }); + + // Add certsAndProxyConfig to request object for bru.sendRequest + request.certsAndProxyConfig = certsAndProxyConfig; + let preRequestScriptResult = null; let preRequestError = null; try { @@ -1288,6 +1302,20 @@ const registerNetworkIpc = (mainWindow) => { } try { + // Build certsAndProxyConfig for bru.sendRequest + const certsAndProxyConfig = await buildCertsAndProxyConfig({ + collectionUid, + collection, + collectionPath, + envVars, + runtimeVariables, + processEnvVars, + request + }); + + // Add certsAndProxyConfig to request object for bru.sendRequest + request.certsAndProxyConfig = certsAndProxyConfig; + let preRequestScriptResult; let preRequestError = null; try { diff --git a/packages/bruno-electron/src/ipc/network/interpolate-string.js b/packages/bruno-electron/src/ipc/network/interpolate-string.js index f79a4e75e..636c819fc 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-string.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-string.js @@ -1,7 +1,7 @@ const { forOwn, cloneDeep } = require('lodash'); -const { interpolate } = require('@usebruno/common'); +const { interpolate, interpolateObject: interpolateObjectCommon } = require('@usebruno/common'); -const interpolateString = (str, { +const buildCombinedVars = ({ globalEnvironmentVariables, collectionVariables, envVars, @@ -11,10 +11,6 @@ const interpolateString = (str, { processEnvVars, promptVariables }) => { - if (!str || !str.length || typeof str !== 'string') { - return str; - } - processEnvVars = processEnvVars || {}; runtimeVariables = runtimeVariables || {}; globalEnvironmentVariables = globalEnvironmentVariables || {}; @@ -38,8 +34,7 @@ const interpolateString = (str, { }); }); - // runtimeVariables take precedence over envVars - const combinedVars = { + return { ...globalEnvironmentVariables, ...collectionVariables, ...envVars, @@ -53,10 +48,26 @@ const interpolateString = (str, { } } }; +}; +const interpolateString = (str, interpolationOptions) => { + if (!str || !str.length || typeof str !== 'string') { + return str; + } + + const combinedVars = buildCombinedVars(interpolationOptions); return interpolate(str, combinedVars); }; -module.exports = { - interpolateString +/** + * Recursively interpolates all string values in an object + */ +const interpolateObject = (obj, interpolationOptions) => { + const combinedVars = buildCombinedVars(interpolationOptions); + return interpolateObjectCommon(obj, combinedVars); +}; + +module.exports = { + interpolateString, + interpolateObject }; diff --git a/packages/bruno-js/src/bru.js b/packages/bruno-js/src/bru.js index 9f9cfe45e..4b0edd795 100644 --- a/packages/bruno-js/src/bru.js +++ b/packages/bruno-js/src/bru.js @@ -1,13 +1,33 @@ const { cloneDeep } = require('lodash'); const xmlFormat = require('xml-formatter'); const { interpolate: _interpolate } = require('@usebruno/common'); -const { sendRequest } = require('@usebruno/requests').scripting; +const { sendRequest, createSendRequest } = require('@usebruno/requests').scripting; const { jar: createCookieJar } = require('@usebruno/requests').cookies; const variableNameRegex = /^[\w-.]*$/; class Bru { - constructor(runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName, promptVariables) { + /** + * @param {string} runtime - The runtime environment ('quickjs' or 'nodevm') + * @param {object} envVariables - Environment variables + * @param {object} runtimeVariables - Runtime variables + * @param {object} processEnvVars - Process environment variables + * @param {string} collectionPath - Path to the collection + * @param {object} collectionVariables - Collection-level variables + * @param {object} folderVariables - Folder-level variables + * @param {object} requestVariables - Request-level variables + * @param {object} globalEnvironmentVariables - Global environment variables + * @param {object} oauth2CredentialVariables - OAuth2 credential variables + * @param {string} collectionName - Name of the collection + * @param {object} promptVariables - Prompt variables + * @param {object} certsAndProxyConfig - Configuration for bru.sendRequest (proxy, certs, TLS) + * @param {string} certsAndProxyConfig.collectionPath - Path to the collection + * @param {object} certsAndProxyConfig.options - TLS and proxy options + * @param {object} [certsAndProxyConfig.clientCertificates] - Client certificate configuration + * @param {object} [certsAndProxyConfig.collectionLevelProxy] - Collection-level proxy settings + * @param {object} [certsAndProxyConfig.systemProxyConfig] - System proxy configuration + */ + constructor(runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName, promptVariables, certsAndProxyConfig) { this.envVariables = envVariables || {}; this.runtimeVariables = runtimeVariables || {}; this.promptVariables = promptVariables || {}; @@ -19,7 +39,8 @@ class Bru { this.oauth2CredentialVariables = oauth2CredentialVariables || {}; this.collectionPath = collectionPath; this.collectionName = collectionName; - this.sendRequest = sendRequest; + // Use createSendRequest with config if provided, otherwise use default sendRequest + this.sendRequest = certsAndProxyConfig ? createSendRequest(certsAndProxyConfig) : sendRequest; this.runtime = runtime; this.cookies = { jar: () => { diff --git a/packages/bruno-js/src/runtime/assert-runtime.js b/packages/bruno-js/src/runtime/assert-runtime.js index 268a1f72e..ba752bf0d 100644 --- a/packages/bruno-js/src/runtime/assert-runtime.js +++ b/packages/bruno-js/src/runtime/assert-runtime.js @@ -256,6 +256,7 @@ class AssertRuntime { } const promptVariables = request?.promptVariables || {}; + const certsAndProxyConfig = request?.certsAndProxyConfig; const bru = new Bru( this.runtime, envVariables, @@ -268,7 +269,8 @@ class AssertRuntime { globalEnvironmentVariables, {}, undefined, - promptVariables + promptVariables, + certsAndProxyConfig ); const req = new BrunoRequest(request); const res = createResponseParser(response); diff --git a/packages/bruno-js/src/runtime/script-runtime.js b/packages/bruno-js/src/runtime/script-runtime.js index 10777947d..d41b51284 100644 --- a/packages/bruno-js/src/runtime/script-runtime.js +++ b/packages/bruno-js/src/runtime/script-runtime.js @@ -33,7 +33,8 @@ class ScriptRuntime { const requestVariables = request?.requestVariables || {}; const promptVariables = request?.promptVariables || {}; const assertionResults = request?.assertionResults || []; - const bru = new Bru(this.runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName, promptVariables); + const certsAndProxyConfig = request?.certsAndProxyConfig; + const bru = new Bru(this.runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName, promptVariables, certsAndProxyConfig); const req = new BrunoRequest(request); // extend bru with result getter methods @@ -128,7 +129,8 @@ class ScriptRuntime { const requestVariables = request?.requestVariables || {}; const promptVariables = request?.promptVariables || {}; const assertionResults = request?.assertionResults || {}; - const bru = new Bru(this.runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName, promptVariables); + const certsAndProxyConfig = request?.certsAndProxyConfig; + const bru = new Bru(this.runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName, promptVariables, certsAndProxyConfig); const req = new BrunoRequest(request); const res = new BrunoResponse(response); diff --git a/packages/bruno-js/src/runtime/test-runtime.js b/packages/bruno-js/src/runtime/test-runtime.js index 927d9d1db..88f30a95c 100644 --- a/packages/bruno-js/src/runtime/test-runtime.js +++ b/packages/bruno-js/src/runtime/test-runtime.js @@ -32,7 +32,8 @@ class TestRuntime { const requestVariables = request?.requestVariables || {}; const promptVariables = request?.promptVariables || {}; const assertionResults = request?.assertionResults || []; - const bru = new Bru(this.runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, {}, collectionName, promptVariables); + const certsAndProxyConfig = request?.certsAndProxyConfig; + const bru = new Bru(this.runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, {}, collectionName, promptVariables, certsAndProxyConfig); const req = new BrunoRequest(request); const res = new BrunoResponse(response); diff --git a/packages/bruno-js/src/runtime/vars-runtime.js b/packages/bruno-js/src/runtime/vars-runtime.js index 5eb58c3d2..d74a9abf3 100644 --- a/packages/bruno-js/src/runtime/vars-runtime.js +++ b/packages/bruno-js/src/runtime/vars-runtime.js @@ -36,7 +36,8 @@ class VarsRuntime { } const promptVariables = request?.promptVariables || {}; - const bru = new Bru(this.runtime, envVariables, runtimeVariables, processEnvVars, undefined, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, undefined, promptVariables); + const certsAndProxyConfig = request?.certsAndProxyConfig; + const bru = new Bru(this.runtime, envVariables, runtimeVariables, processEnvVars, undefined, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, undefined, promptVariables, certsAndProxyConfig); const req = new BrunoRequest(request); const res = createResponseParser(response); diff --git a/packages/bruno-requests/src/scripting/index.ts b/packages/bruno-requests/src/scripting/index.ts index 7dd459330..2cb147b73 100644 --- a/packages/bruno-requests/src/scripting/index.ts +++ b/packages/bruno-requests/src/scripting/index.ts @@ -1 +1 @@ -export { default as sendRequest } from './send-request'; +export { default as sendRequest, createSendRequest } from './send-request'; diff --git a/packages/bruno-requests/src/scripting/send-request.spec.ts b/packages/bruno-requests/src/scripting/send-request.spec.ts new file mode 100644 index 000000000..d785ff9b4 --- /dev/null +++ b/packages/bruno-requests/src/scripting/send-request.spec.ts @@ -0,0 +1,198 @@ +import sendRequest, { createSendRequest } from './send-request'; + +jest.mock('../network', () => ({ + makeAxiosInstance: jest.fn() +})); + +jest.mock('../utils/http-https-agents', () => ({ + getHttpHttpsAgents: jest.fn() +})); + +import { makeAxiosInstance } from '../network'; +import { getHttpHttpsAgents } from '../utils/http-https-agents'; + +const mockMakeAxiosInstance = makeAxiosInstance as jest.Mock; +const mockGetHttpHttpsAgents = getHttpHttpsAgents as jest.Mock; + +describe('sendRequest', () => { + let mockAxios: jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); + mockAxios = jest.fn(); + mockMakeAxiosInstance.mockReturnValue(mockAxios); + mockGetHttpHttpsAgents.mockResolvedValue({ httpAgent: null, httpsAgent: null }); + }); + + describe('without callback', () => { + test('should return response directly', async () => { + const mockResponse = { data: 'test', status: 200 }; + mockAxios.mockResolvedValue(mockResponse); + + const result = await sendRequest({ url: 'http://example.com' }); + + expect(result).toBe(mockResponse); + }); + + test('should reject on request error', async () => { + const error = new Error('Network error'); + mockAxios.mockRejectedValue(error); + + await expect(sendRequest({ url: 'http://example.com' })).rejects.toThrow('Network error'); + }); + + test('should handle URL string instead of config object', async () => { + const mockResponse = { data: 'pong', status: 200 }; + mockAxios.mockResolvedValue(mockResponse); + + const result = await sendRequest('http://example.com/ping'); + + expect(result).toBe(mockResponse); + expect(mockAxios).toHaveBeenCalledWith( + expect.objectContaining({ + url: 'http://example.com/ping' + }) + ); + }); + }); + + describe('with callback', () => { + test('should call callback with response and return response', async () => { + const mockResponse = { data: 'test', status: 200 }; + mockAxios.mockResolvedValue(mockResponse); + const callback = jest.fn(); + + const result = await sendRequest({ url: 'http://example.com' }, callback); + + expect(callback).toHaveBeenCalledWith(null, mockResponse); + expect(result).toBe(mockResponse); + }); + + test('should call callback with error on request failure', async () => { + const error = new Error('Network error'); + mockAxios.mockRejectedValue(error); + const callback = jest.fn(); + + await sendRequest({ url: 'http://example.com' }, callback); + + expect(callback).toHaveBeenCalledWith(error, null); + }); + + test('should reject if callback throws on success', async () => { + const mockResponse = { data: 'test', status: 200 }; + mockAxios.mockResolvedValue(mockResponse); + const callbackError = new Error('Callback error'); + const callback = jest.fn().mockRejectedValue(callbackError); + + await expect(sendRequest({ url: 'http://example.com' }, callback)).rejects.toThrow( + 'Callback error' + ); + }); + + test('should reject if callback throws on error', async () => { + const requestError = new Error('Network error'); + mockAxios.mockRejectedValue(requestError); + const callbackError = new Error('Callback error'); + const callback = jest.fn().mockRejectedValue(callbackError); + + await expect(sendRequest({ url: 'http://example.com' }, callback)).rejects.toThrow( + 'Callback error' + ); + }); + }); +}); + +describe('createSendRequest', () => { + let mockAxios: jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); + mockAxios = jest.fn(); + mockMakeAxiosInstance.mockReturnValue(mockAxios); + }); + + test('should apply agents from config', async () => { + const mockHttpAgent = { name: 'httpAgent' }; + const mockHttpsAgent = { name: 'httpsAgent' }; + mockGetHttpHttpsAgents.mockResolvedValue({ + httpAgent: mockHttpAgent, + httpsAgent: mockHttpsAgent + }); + const mockResponse = { data: 'test' }; + mockAxios.mockResolvedValue(mockResponse); + + const customSendRequest = createSendRequest({ proxyConfig: {} }); + await customSendRequest({ url: 'https://example.com' }); + + expect(mockGetHttpHttpsAgents).toHaveBeenCalledWith({ + proxyConfig: {}, + requestUrl: 'https://example.com' + }); + expect(mockAxios).toHaveBeenCalledWith( + expect.objectContaining({ + httpAgent: mockHttpAgent, + httpsAgent: mockHttpsAgent + }) + ); + }); + + test('should not override agents if already set in requestConfig', async () => { + const configHttpAgent = { name: 'configAgent' }; + const configHttpsAgent = { name: 'configHttpsAgent' }; + mockGetHttpHttpsAgents.mockResolvedValue({ + httpAgent: { name: 'ignored' }, + httpsAgent: { name: 'ignored' } + }); + mockAxios.mockResolvedValue({ data: 'test' }); + + const customSendRequest = createSendRequest({ proxyConfig: {} }); + await customSendRequest({ + url: 'https://example.com', + httpAgent: configHttpAgent, + httpsAgent: configHttpsAgent + }); + + expect(mockAxios).toHaveBeenCalledWith( + expect.objectContaining({ + httpAgent: configHttpAgent, + httpsAgent: configHttpsAgent + }) + ); + }); + + test('should not call getHttpHttpsAgents when no config provided', async () => { + mockAxios.mockResolvedValue({ data: 'test' }); + + const customSendRequest = createSendRequest(); + await customSendRequest({ url: 'https://example.com' }); + + expect(mockGetHttpHttpsAgents).not.toHaveBeenCalled(); + }); + + test('should handle URL string and apply agents from config', async () => { + const mockHttpAgent = { name: 'httpAgent' }; + const mockHttpsAgent = { name: 'httpsAgent' }; + mockGetHttpHttpsAgents.mockResolvedValue({ + httpAgent: mockHttpAgent, + httpsAgent: mockHttpsAgent + }); + const mockResponse = { data: 'pong' }; + mockAxios.mockResolvedValue(mockResponse); + + const customSendRequest = createSendRequest({ collectionPath: '/test' }); + const result = await customSendRequest('https://example.com/ping'); + + expect(result).toBe(mockResponse); + expect(mockGetHttpHttpsAgents).toHaveBeenCalledWith({ + collectionPath: '/test', + requestUrl: 'https://example.com/ping' + }); + expect(mockAxios).toHaveBeenCalledWith( + expect.objectContaining({ + url: 'https://example.com/ping', + httpAgent: mockHttpAgent, + httpsAgent: mockHttpsAgent + }) + ); + }); +}); diff --git a/packages/bruno-requests/src/scripting/send-request.ts b/packages/bruno-requests/src/scripting/send-request.ts index 926e8143e..2564fde52 100644 --- a/packages/bruno-requests/src/scripting/send-request.ts +++ b/packages/bruno-requests/src/scripting/send-request.ts @@ -1,27 +1,76 @@ import { AxiosRequestConfig } from 'axios'; import { makeAxiosInstance } from '../network'; +import { getHttpHttpsAgents } from '../utils/http-https-agents'; +import type { GetHttpHttpsAgentsParams } from '../utils/http-https-agents'; type T_SendRequestCallback = (error: any, response: any) => void; -const sendRequest = async (requestConfig: AxiosRequestConfig, callback: T_SendRequestCallback) => { - const axiosInstance = makeAxiosInstance(); - if (!callback) { - return await axiosInstance(requestConfig); - } - try { - const response = await axiosInstance(requestConfig); +/** + * Configuration for creating a sendRequest function with proxy/certs support. + * This is the same config used by getHttpHttpsAgents, minus requestUrl which is + * extracted from the actual request. + */ +type SendRequestConfig = Omit; + +/** + * Creates a sendRequest function configured with proxy and certificate settings. + * This allows bru.sendRequest to use the same proxy/certs config as the main request. + * + * @param config - Configuration for proxy, certs, and TLS options (same as getHttpHttpsAgents) + * @returns A sendRequest function that applies the config to each request + */ +const createSendRequest = (config?: SendRequestConfig) => { + return async (requestConfig: AxiosRequestConfig | string, callback?: T_SendRequestCallback) => { + // Handle case where requestConfig is a URL string + const normalizedConfig: AxiosRequestConfig = typeof requestConfig === 'string' + ? { url: requestConfig } + : { ...requestConfig }; + + // If config is provided, create agents with the request URL for proper proxy bypass + if (config) { + const requestUrl = normalizedConfig.url; + + const { httpAgent, httpsAgent } = await getHttpHttpsAgents({ + ...config, + requestUrl + }); + + // Apply agents if not explicitly set in normalizedConfig + if (httpAgent && !normalizedConfig.httpAgent) { + normalizedConfig.httpAgent = httpAgent; + } + if (httpsAgent && !normalizedConfig.httpsAgent) { + normalizedConfig.httpsAgent = httpsAgent; + } + } + + const axiosInstance = makeAxiosInstance(); + + if (!callback) { + return await axiosInstance(normalizedConfig); + } + try { - await callback(null, response); + const response = await axiosInstance(normalizedConfig); + try { + await callback(null, response); + return response; + } catch (error) { + return Promise.reject(error); + } } catch (error) { - return Promise.reject(error); + try { + await callback(error, null); + } catch (err) { + return Promise.reject(err); + } } - } catch (error) { - try { - await callback(error, null); - } catch (err) { - return Promise.reject(err); - } - } + }; }; +// Default sendRequest without config (for backward compatibility) +const sendRequest = createSendRequest(); + export default sendRequest; +export { createSendRequest }; +export type { SendRequestConfig }; diff --git a/packages/bruno-requests/src/utils/http-https-agents.ts b/packages/bruno-requests/src/utils/http-https-agents.ts index a34bb8ea0..8e5eef95b 100644 --- a/packages/bruno-requests/src/utils/http-https-agents.ts +++ b/packages/bruno-requests/src/utils/http-https-agents.ts @@ -214,18 +214,18 @@ const getCertsAndProxyConfig = ({ }: GetCertsAndProxyConfigParams): GetCertsAndProxyConfigResult => { const certsConfig: CertsConfig = {}; - const caCertFilePath = options.shouldUseCustomCaCertificate && options.customCaCertificateFilePath ? options.customCaCertificateFilePath : undefined; - const caCertificatesData = getCACertificates({ - caCertFilePath, - shouldKeepDefaultCerts: options.shouldKeepDefaultCaCertificates - }); + // Only load CA certificates when TLS verification is enabled + if (options.shouldVerifyTls) { + const caCertFilePath = options.shouldUseCustomCaCertificate && options.customCaCertificateFilePath ? options.customCaCertificateFilePath : undefined; + const caCertificatesData = getCACertificates({ + caCertFilePath, + shouldKeepDefaultCerts: options.shouldKeepDefaultCaCertificates + }); - const caCertificates = caCertificatesData.caCertificates; - const caCertificatesCount = caCertificatesData.caCertificatesCount; - - // configure HTTPS agent with aggregated CA certificates - certsConfig.caCertificatesCount = caCertificatesCount; - certsConfig.ca = caCertificates || []; + // configure HTTPS agent with aggregated CA certificates + certsConfig.caCertificatesCount = caCertificatesData.caCertificatesCount; + certsConfig.ca = caCertificatesData.caCertificates || []; + } // client certificate config const clientCertConfig = get(clientCertificates, 'certs', []) as ClientCertificate[]; @@ -443,3 +443,5 @@ const getHttpHttpsAgents = async ({ }; export { getHttpHttpsAgents }; + +export type { GetHttpHttpsAgentsParams };