diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index 7be8743ff..eb635fdce 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -429,10 +429,12 @@ const runSingleRequest = async function ( // Prepare TLS options for agent caching const tlsOptions = { - ...httpsAgentRequestFields, - keepAlive: true + ...httpsAgentRequestFields }; + // HTTP agent options — separate from tlsOptions to avoid leaking TLS fields + const httpAgentOptions = { keepAlive: true }; + const parsedRequestUrl = new URL(request.url); const isHttpsRequest = parsedRequestUrl.protocol === 'https:'; @@ -459,13 +461,13 @@ const runSingleRequest = async function ( if (isHttpsRequest) { request.httpsAgent = getOrCreateAgent(SocksProxyAgent, tlsOptions, proxyUri); } else { - request.httpAgent = getOrCreateHttpAgent(SocksProxyAgent, { keepAlive: true }, proxyUri); + request.httpAgent = getOrCreateHttpAgent(SocksProxyAgent, httpAgentOptions, proxyUri); } } else { if (isHttpsRequest) { request.httpsAgent = getOrCreateAgent(PatchedHttpsProxyAgent, tlsOptions, proxyUri); } else { - request.httpAgent = getOrCreateHttpAgent(HttpProxyAgent, { keepAlive: true }, proxyUri); + request.httpAgent = getOrCreateHttpAgent(HttpProxyAgent, httpAgentOptions, proxyUri); } } } @@ -477,7 +479,7 @@ const runSingleRequest = async function ( try { if (http_proxy?.length && !isHttpsRequest) { new URL(http_proxy); - request.httpAgent = getOrCreateHttpAgent(HttpProxyAgent, { keepAlive: true }, http_proxy); + request.httpAgent = getOrCreateHttpAgent(HttpProxyAgent, httpAgentOptions, http_proxy); } } catch (error) { throw new Error('Invalid system http_proxy'); @@ -501,7 +503,7 @@ const runSingleRequest = async function ( if (isHttpsRequest) { request.httpsAgent = getOrCreateAgent(https.Agent, tlsOptions, null); } else { - request.httpAgent = getOrCreateHttpAgent(http.Agent, { keepAlive: true }, null); + request.httpAgent = getOrCreateHttpAgent(http.Agent, httpAgentOptions, null); } } @@ -609,7 +611,7 @@ const runSingleRequest = async function ( let token; if (oauth2RequestUrl) { - const tlsOptions = { + const oauth2ConfigOptions = { noproxy: options.noproxy, shouldVerifyTls: !insecure, shouldUseCustomCaCertificate: !!options['cacert'], @@ -626,7 +628,7 @@ const runSingleRequest = async function ( const { httpAgent: oauth2HttpAgent, httpsAgent: oauth2HttpsAgent } = await getHttpHttpsAgents({ requestUrl: oauth2RequestUrl, collectionPath, - options: tlsOptions, + options: oauth2ConfigOptions, clientCertificates: interpolatedClientCertificates, collectionLevelProxy: interpolatedProxyConfig, systemProxyConfig diff --git a/packages/bruno-electron/src/store/preferences.js b/packages/bruno-electron/src/store/preferences.js index a37d363a7..20a0888d1 100644 --- a/packages/bruno-electron/src/store/preferences.js +++ b/packages/bruno-electron/src/store/preferences.js @@ -106,6 +106,11 @@ const defaultPreferences = { }, display: { zoomPercentage: 100 + }, + cache: { + httpHttpsAgents: { + enabled: true + } } }; @@ -164,7 +169,12 @@ const preferencesSchema = Yup.object().shape({ }), display: Yup.object({ zoomPercentage: Yup.number().min(50).max(150) - }) + }), + cache: Yup.object({ + httpHttpsAgents: Yup.object({ + enabled: Yup.boolean() + }) + }).optional() }); class PreferencesStore { @@ -351,6 +361,9 @@ const preferencesUtil = { getZoomPercentage: () => { return get(getPreferences(), 'display.zoomPercentage', 100); }, + isHttpHttpsAgentCachingEnabled: () => { + return get(getPreferences(), 'cache.httpHttpsAgents.enabled', true); + }, hasLaunchedBefore: () => { return get(getPreferences(), 'onboarding.hasLaunchedBefore', false); }, diff --git a/packages/bruno-electron/src/utils/proxy-util.js b/packages/bruno-electron/src/utils/proxy-util.js index 7a6fcd1d3..25863ad34 100644 --- a/packages/bruno-electron/src/utils/proxy-util.js +++ b/packages/bruno-electron/src/utils/proxy-util.js @@ -6,7 +6,7 @@ const { interpolateString } = require('../ipc/network/interpolate-string'); const { SocksProxyAgent } = require('socks-proxy-agent'); const { HttpProxyAgent } = require('http-proxy-agent'); const { isEmpty, get, isUndefined, isNull } = require('lodash'); -const { getOrCreateAgent, getOrCreateHttpAgent, clearAgentCache, getAgentCacheSize } = require('@usebruno/requests'); +const { getOrCreateAgent, getOrCreateHttpAgent } = require('@usebruno/requests'); const DEFAULT_PORTS = { ftp: 21, @@ -200,9 +200,5 @@ function setupProxyAgents({ module.exports = { shouldUseProxy, PatchedHttpsProxyAgent, - setupProxyAgents, - clearAgentCache, - getAgentCacheSize, - getOrCreateAgent, - getOrCreateHttpAgent + setupProxyAgents }; diff --git a/packages/bruno-requests/src/utils/agent-cache.spec.ts b/packages/bruno-requests/src/utils/agent-cache.spec.ts index ab1cdc0d4..443039d56 100644 --- a/packages/bruno-requests/src/utils/agent-cache.spec.ts +++ b/packages/bruno-requests/src/utils/agent-cache.spec.ts @@ -61,6 +61,14 @@ describe('Agent Cache', () => { expect(httpsAgent).not.toBe(httpAgent); expect(getAgentCacheSize()).toBe(2); }); + + it('creates separate agents for different keepAlive values', () => { + const agent1 = getOrCreateAgent(https.Agent, { keepAlive: true }); + const agent2 = getOrCreateAgent(https.Agent, { keepAlive: false }); + + expect(agent1).not.toBe(agent2); + expect(getAgentCacheSize()).toBe(2); + }); }); describe('timeline support', () => { diff --git a/packages/bruno-requests/src/utils/agent-cache.ts b/packages/bruno-requests/src/utils/agent-cache.ts index 17eca7ccb..837df9035 100644 --- a/packages/bruno-requests/src/utils/agent-cache.ts +++ b/packages/bruno-requests/src/utils/agent-cache.ts @@ -1,6 +1,4 @@ import crypto from 'node:crypto'; -import http from 'node:http'; -import https from 'node:https'; import type { Agent as HttpAgent } from 'node:http'; import type { Agent as HttpsAgent } from 'node:https'; import { createTimelineAgentClass, createTimelineHttpAgentClass, type TimelineEntry, type AgentOptions, type HttpAgentOptions, type AgentClass, type HttpAgentClass } from './timeline-agent'; @@ -82,6 +80,7 @@ function getAgentCacheKey(agentClassId: number, options: AgentOptions, proxyUri: const keyData = { agentClassId, proxyUri, + keepAlive: options.keepAlive, rejectUnauthorized: options.rejectUnauthorized, // Hash certificates and passphrase instead of including full content ca: hashCaValue(options.ca), @@ -233,7 +232,7 @@ function getOrCreateAgent( timeline, getAgentCacheKey, getTimelineAgentClass, - 'Reusing cached agent (SSL session reuse enabled)' + 'Reusing cached https agent' ); } @@ -261,7 +260,7 @@ function getOrCreateHttpAgent( timeline, getHttpAgentCacheKey, getTimelineHttpAgentClass, - 'Reusing cached agent (connection reuse enabled)' + 'Reusing cached http agent' ) as HttpAgent; } diff --git a/packages/bruno-requests/src/utils/http-https-agents.ts b/packages/bruno-requests/src/utils/http-https-agents.ts index 25c6e9cce..476490a92 100644 --- a/packages/bruno-requests/src/utils/http-https-agents.ts +++ b/packages/bruno-requests/src/utils/http-https-agents.ts @@ -1,5 +1,6 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; +import http from 'node:http'; import https from 'node:https'; import type { Agent as HttpAgent } from 'node:http'; import type { Agent as HttpsAgent } from 'node:https'; @@ -11,6 +12,7 @@ import { isEmpty, get, isUndefined, isNull } from 'lodash'; import { getCACertificates } from './ca-cert'; import { transformProxyConfig } from './proxy-util'; import { getOrCreateAgent, getOrCreateHttpAgent } from './agent-cache'; +import type { TimelineEntry } from './timeline-agent'; const DEFAULT_PORTS: Record = { ftp: 21, @@ -114,12 +116,6 @@ type GetCertsAndProxyConfigResult = { certsConfig: CertsConfig; }; -type TimelineEntry = { - timestamp: Date; - type: 'info' | 'tls' | 'error'; - message: string; -}; - type CreateAgentsParams = { requestUrl?: string; proxyMode: ProxyMode; @@ -368,9 +364,10 @@ function createAgents({ let httpAgent: HttpAgent | undefined; let httpsAgent: HttpsAgent | HttpsProxyAgent | SocksProxyAgent | undefined; + // Determine if this is an HTTPS request + const isHttpsRequest = requestUrl ? requestUrl.startsWith('https:') : true; + if (proxyMode === 'on') { - // Determine if this is an HTTPS request - const isHttpsRequest = requestUrl ? requestUrl.startsWith('https:') : true; const shouldProxy = shouldUseProxy(requestUrl, get(proxyConfig, 'bypassProxy', '')); if (shouldProxy) { const proxyProtocol = get(proxyConfig, 'protocol'); @@ -421,7 +418,7 @@ function createAgents({ const shouldUseSystemProxy = shouldUseProxy(requestUrl, no_proxy || ''); if (shouldUseSystemProxy) { try { - if (http_proxy?.length) { + if (http_proxy?.length && !isHttpsRequest) { new URL(http_proxy); httpAgent = getOrCreateHttpAgent(HttpProxyAgent, { keepAlive: true }, http_proxy, timeline || null); } @@ -429,20 +426,22 @@ function createAgents({ throw new Error('Invalid system http_proxy'); } try { - if (https_proxy?.length) { + if (https_proxy?.length && isHttpsRequest) { new URL(https_proxy); httpsAgent = getOrCreateAgent(PatchedHttpsProxyAgent, tlsOptions as any, https_proxy, timeline || null) as HttpsAgent; - } else { - httpsAgent = getOrCreateAgent(https.Agent, tlsOptions as any, null, timeline || null) as HttpsAgent; } } catch (error) { throw new Error('Invalid system https_proxy'); } - } else { - httpsAgent = getOrCreateAgent(https.Agent, tlsOptions as any, null, timeline || null) as HttpsAgent; } - } else { - httpsAgent = getOrCreateAgent(https.Agent, tlsOptions as any, null, timeline || null) as HttpsAgent; + } + + if (!httpAgent && !httpsAgent) { + if (isHttpsRequest) { + httpsAgent = getOrCreateAgent(https.Agent, tlsOptions as any, null, timeline || null) as HttpsAgent; + } else { + httpAgent = getOrCreateHttpAgent(http.Agent, { keepAlive: true }, null, timeline || null); + } } return { httpAgent, httpsAgent };