fix(proxy): stabilize upstream fetch transport

Refs #1686
This commit is contained in:
Tam Nhu Tran committed 2026-08-08 21:17:13 -04:00
1 parent 7fb56d62e1
commit d8210cf011
17 files changed
+889 -85

No files matched your search

@@ -0,0 +1,100 @@
const http = require('node:http');
const path = require('node:path');
const repoRoot = process.env.CCS_RUNTIME_PROBE_ROOT || process.cwd();
const {
closeOpenAICompatProxyServer,
startOpenAICompatProxyServer,
} = require(path.join(repoRoot, 'dist/proxy/server/proxy-server.js'));
function waitForListening(server) {
if (server.listening) return Promise.resolve();
return new Promise((resolve, reject) => {
server.once('error', reject);
server.once('listening', resolve);
});
}
function request(port, requestPath, options = {}) {
return new Promise((resolve, reject) => {
const req = http.request(
{
hostname: '127.0.0.1',
port,
path: requestPath,
method: options.method || 'GET',
headers: options.headers,
agent: options.agent,
},
(res) => {
let body = '';
res.setEncoding('utf8');
res.on('data', (chunk) => (body += chunk));
res.on('end', () => resolve({ status: res.statusCode || 0, body }));
}
);
req.once('error', reject);
req.end(options.body);
});
}
async function startProbeServer() {
const server = startOpenAICompatProxyServer({
profile: {
profileName: 'sentinel',
settingsPath: '/tmp/sentinel.settings.json',
baseUrl: process.env.CCS_RUNTIME_PROBE_UPSTREAM,
apiKey: 'sentinel-api-key-not-real',
provider: 'generic-chat-completion-api',
model: 'sentinel-model',
passthrough: false,
},
port: 0,
authToken: 'sentinel-local-token',
});
await waitForListening(server);
return server;
}
async function runTransportProbe() {
const server = await startProbeServer();
const port = server.address().port;
const response = await request(port, '/v1/messages', {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-api-key': 'sentinel-local-token',
},
body: JSON.stringify({
model: 'sentinel-model',
messages: [{ role: 'user', content: 'hi' }],
}),
});
await closeOpenAICompatProxyServer(server, 250);
const parsed = JSON.parse(response.body);
console.log(
JSON.stringify({
status: response.status,
text: parsed.content?.[0]?.text,
error: parsed.error?.message,
})
);
}
async function runShutdownProbe() {
const server = await startProbeServer();
const port = server.address().port;
const agent = new http.Agent({ keepAlive: true });
await request(port, '/health', { agent });
const startedAt = Date.now();
await closeOpenAICompatProxyServer(server, 250);
const elapsedMs = Date.now() - startedAt;
agent.destroy();
console.log(JSON.stringify({ closed: !server.listening, elapsedMs }));
}
const mode = process.env.CCS_RUNTIME_PROBE_MODE || 'transport';
Promise.resolve(mode === 'shutdown' ? runShutdownProbe() : runTransportProbe()).catch((error) => {
console.error(JSON.stringify({ name: error.name, message: error.message }));
process.exitCode = 1;
});
@@ -0,0 +1,200 @@
import { afterAll, beforeAll, describe, expect, it } from 'bun:test';
import { spawn, spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as http from 'node:http';
import * as os from 'node:os';
import * as path from 'node:path';
const matrixEnabled = process.env.CCS_RUNTIME_MATRIX === '1';
const fixturePath = path.join(process.cwd(), 'tests/integration/proxy/fixtures/runtime-probe.cjs');
const proxyKeys = [
'HTTP_PROXY',
'HTTPS_PROXY',
'ALL_PROXY',
'NO_PROXY',
'http_proxy',
'https_proxy',
'all_proxy',
'no_proxy',
];
type ProbeResult = {
status?: number;
text?: string;
error?: string;
closed?: boolean;
elapsedMs?: number;
};
function resolveNodeBinary(version: 18 | 22 | 26): string {
const result = spawnSync('npx', ['-y', `node@${version}`, '-p', 'process.execPath'], {
encoding: 'utf8',
env: Object.fromEntries(
Object.entries(process.env).filter(([key]) => !proxyKeys.includes(key))
),
});
if (result.status !== 0) {
throw new Error(`Unable to resolve Node ${version}: ${result.stderr.trim()}`);
}
return result.stdout.trim();
}
function runProbe(
binary: string,
upstream: string,
env: Record<string, string> = {},
mode = 'transport'
): Promise<ProbeResult> {
const cleanEnv = Object.fromEntries(
Object.entries(process.env).filter(([key]) => !proxyKeys.includes(key))
);
const ccsHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-runtime-matrix-'));
return new Promise((resolve, reject) => {
const child = spawn(binary, [fixturePath], {
cwd: process.cwd(),
env: {
...cleanEnv,
...env,
CCS_RUNTIME_PROBE_ROOT: process.cwd(),
CCS_RUNTIME_PROBE_UPSTREAM: upstream,
CCS_RUNTIME_PROBE_MODE: mode,
CCS_HOME: ccsHome,
},
stdio: ['ignore', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (chunk) => (stdout += chunk));
child.stderr.on('data', (chunk) => (stderr += chunk));
child.once('error', (error) => {
fs.rmSync(ccsHome, { recursive: true, force: true });
reject(error);
});
child.once('close', (code) => {
fs.rmSync(ccsHome, { recursive: true, force: true });
if (code !== 0) {
reject(new Error(`Probe exited ${code}: ${stderr || stdout}`));
return;
}
const lastLine = stdout.trim().split('\n').pop();
if (!lastLine) {
reject(new Error(`Probe returned no output: ${stderr}`));
return;
}
resolve(JSON.parse(lastLine) as ProbeResult);
});
});
}
describe.skipIf(!matrixEnabled)('real runtime upstream transport matrix', () => {
let node18 = '';
let node22 = '';
let node26 = '';
let upstreamServer: http.Server | undefined;
let upstreamPort = 0;
let corporateProxy: http.Server | undefined;
let corporateProxyPort = 0;
let corporateProxyHits = 0;
let sawProxyAuthorization = false;
beforeAll(async () => {
node18 = resolveNodeBinary(18);
node22 = resolveNodeBinary(22);
node26 = resolveNodeBinary(26);
const payload = JSON.stringify({
id: 'chatcmpl_matrix',
model: 'sentinel-model',
choices: [
{
index: 0,
message: { role: 'assistant', content: 'matrix-ok' },
finish_reason: 'stop',
},
],
usage: { prompt_tokens: 1, completion_tokens: 1 },
});
upstreamServer = http.createServer((_req, res) => {
res.writeHead(200, { 'content-type': 'application/json' });
res.end(payload);
});
corporateProxy = http.createServer((req, res) => {
corporateProxyHits += 1;
sawProxyAuthorization = String(req.headers['proxy-authorization'] || '').startsWith('Basic ');
res.writeHead(200, {
'content-type': 'application/json',
'content-length': Buffer.byteLength(payload),
});
res.end(payload);
});
corporateProxy.on('connect', (req, socket) => {
corporateProxyHits += 1;
sawProxyAuthorization = String(req.headers['proxy-authorization'] || '').startsWith('Basic ');
socket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
socket.once('data', () => {
socket.end(
`HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: ${Buffer.byteLength(payload)}\r\nConnection: close\r\n\r\n${payload}`
);
});
});
await Promise.all([
new Promise<void>((resolve) => upstreamServer.listen(0, '127.0.0.1', resolve)),
new Promise<void>((resolve) => corporateProxy.listen(0, '127.0.0.1', resolve)),
]);
upstreamPort = (upstreamServer.address() as { port: number }).port;
corporateProxyPort = (corporateProxy.address() as { port: number }).port;
}, 60_000);
afterAll(async () => {
const servers = [upstreamServer, corporateProxy].filter(
(server): server is http.Server => server !== undefined
);
await Promise.all(
servers.map((server) => new Promise<void>((resolve) => server.close(() => resolve())))
);
}, 10_000);
it('uses the matching fetch transport on Node 18, 22, 26, and Bun', async () => {
const upstream = `http://127.0.0.1:${upstreamPort}/v1`;
const results = await Promise.all([
runProbe(node18, upstream),
runProbe(node22, upstream),
runProbe(node26, upstream),
runProbe(process.execPath, upstream),
]);
for (const result of results) {
expect(result).toMatchObject({ status: 200, text: 'matrix-ok' });
}
});
it('routes Node 26 and Bun through a real credentialed proxy', async () => {
const proxyUrl = `http://sentinel-user:sentinel-password@127.0.0.1:${corporateProxyPort}`;
const nodeResult = await runProbe(node26, 'http://upstream.invalid/v1', {
HTTP_PROXY: proxyUrl,
NO_PROXY: '127.0.0.1,localhost',
});
const bunResult = await runProbe(process.execPath, 'http://upstream.invalid/v1', {
HTTP_PROXY: proxyUrl,
NO_PROXY: '127.0.0.1,localhost',
});
expect(nodeResult).toMatchObject({ status: 200, text: 'matrix-ok' });
expect(bunResult).toMatchObject({ status: 200, text: 'matrix-ok' });
expect(corporateProxyHits).toBeGreaterThanOrEqual(2);
expect(sawProxyAuthorization).toBe(true);
});
it('honors NO_PROXY without touching the proxy', async () => {
const hitsBefore = corporateProxyHits;
const result = await runProbe(node26, 'http://upstream.invalid/v1', {
HTTP_PROXY: `http://sentinel-user:sentinel-password@127.0.0.1:${corporateProxyPort}`,
NO_PROXY: 'upstream.invalid',
});
expect(result.status).toBe(502);
expect(corporateProxyHits).toBe(hitsBefore);
});
it('shuts down Node 18 with an idle keep-alive connection', async () => {
const result = await runProbe(node18, `http://127.0.0.1:${upstreamPort}/v1`, {}, 'shutdown');
expect(result.closed).toBe(true);
expect(result.elapsedMs).toBeLessThan(2_000);
});
});
@@ -0,0 +1,195 @@
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import * as fs from 'node:fs';
import * as http from 'node:http';
import * as os from 'node:os';
import * as path from 'node:path';
import { getGlobalDispatcher } from 'undici';
import {
closeOpenAICompatProxyServer,
startOpenAICompatProxyServer,
} from '../../../src/proxy/server/proxy-server';
import type { OpenAICompatProfileConfig } from '../../../src/proxy/profile-router';
const PROXY_ENV_KEYS = ['HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY'] as const;
function listen(server: http.Server): Promise<number> {
return new Promise((resolve, reject) => {
server.once('error', reject);
server.listen(0, '127.0.0.1', () => {
const address = server.address();
if (!address || typeof address === 'string') {
reject(new Error('Failed to resolve listening port'));
return;
}
resolve(address.port);
});
});
}
function requestMessages(port: number): Promise<{ status: number; body: string }> {
return new Promise((resolve, reject) => {
const req = http.request(
{
hostname: '127.0.0.1',
port,
path: '/v1/messages',
method: 'POST',
headers: {
'content-type': 'application/json',
'x-api-key': 'sentinel-local-token',
},
},
(res) => {
let body = '';
res.setEncoding('utf8');
res.on('data', (chunk) => (body += chunk));
res.on('end', () => resolve({ status: res.statusCode ?? 0, body }));
}
);
req.once('error', reject);
req.end(
JSON.stringify({
model: 'sentinel-model',
messages: [{ role: 'user', content: 'hi' }],
})
);
});
}
describe('runtime-aware upstream transport', () => {
const originalEnv = new Map<string, string | undefined>();
let ccsHome = '';
let corporateProxy: http.Server | undefined;
let corporateProxyHits = 0;
let sawProxyAuthorization = false;
let proxyServer: http.Server | undefined;
beforeEach(() => {
for (const key of PROXY_ENV_KEYS) {
originalEnv.set(key, process.env[key]);
delete process.env[key];
}
originalEnv.set('CCS_HOME', process.env.CCS_HOME);
ccsHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-runtime-transport-'));
process.env.CCS_HOME = ccsHome;
corporateProxy = undefined;
proxyServer = undefined;
corporateProxyHits = 0;
sawProxyAuthorization = false;
});
afterEach(async () => {
if (proxyServer) {
await closeOpenAICompatProxyServer(proxyServer, 50);
}
if (corporateProxy) {
await new Promise<void>((resolve) => corporateProxy.close(() => resolve()));
}
for (const key of [...PROXY_ENV_KEYS, 'CCS_HOME'] as const) {
const value = originalEnv.get(key);
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
fs.rmSync(ccsHome, { recursive: true, force: true });
});
it('uses a credentialed proxy and honors NO_PROXY without global fetch mutation', async () => {
const originalFetch = globalThis.fetch;
const upstreamPayload = JSON.stringify({
id: 'chatcmpl_proxy',
model: 'sentinel-model',
choices: [
{
index: 0,
message: { role: 'assistant', content: 'proxied-ok' },
finish_reason: 'stop',
},
],
usage: { prompt_tokens: 1, completion_tokens: 1 },
});
corporateProxy = http.createServer((req, res) => {
corporateProxyHits += 1;
sawProxyAuthorization = String(req.headers['proxy-authorization'] || '').startsWith('Basic ');
res.writeHead(200, {
'content-type': 'application/json',
'content-length': Buffer.byteLength(upstreamPayload),
});
res.end(upstreamPayload);
});
const corporateProxyPort = await listen(corporateProxy);
process.env.HTTP_PROXY = `http://sentinel-user:sentinel-password@127.0.0.1:${corporateProxyPort}`;
const profile: OpenAICompatProfileConfig = {
profileName: 'sentinel',
settingsPath: '/tmp/sentinel.settings.json',
baseUrl: 'http://upstream.invalid/v1',
apiKey: 'sentinel-api-key-not-real',
provider: 'generic-chat-completion-api',
model: 'sentinel-model',
};
proxyServer = startOpenAICompatProxyServer({
profile,
port: 0,
authToken: 'sentinel-local-token',
});
const proxyPort = await new Promise<number>((resolve, reject) => {
proxyServer.once('error', reject);
proxyServer.once('listening', () => {
const address = proxyServer.address();
if (!address || typeof address === 'string') reject(new Error('Missing proxy port'));
else resolve(address.port);
});
});
const proxied = await requestMessages(proxyPort);
expect(proxied.status).toBe(200);
expect(JSON.parse(proxied.body)).toMatchObject({
content: [{ type: 'text', text: 'proxied-ok' }],
});
expect(corporateProxyHits).toBe(1);
expect(sawProxyAuthorization).toBe(true);
process.env.NO_PROXY = 'upstream.invalid';
const hitsBeforeBypass = corporateProxyHits;
const bypassed = await requestMessages(proxyPort);
expect(bypassed.status).toBe(502);
expect(corporateProxyHits).toBe(hitsBeforeBypass);
expect(globalThis.fetch).toBe(originalFetch);
});
it('keeps global transport ownership unchanged across overlapping server lifecycles', async () => {
const originalFetch = globalThis.fetch;
const originalDispatcher = getGlobalDispatcher();
const profile: OpenAICompatProfileConfig = {
profileName: 'sentinel',
settingsPath: '/tmp/sentinel.settings.json',
baseUrl: 'http://127.0.0.1:1/v1',
apiKey: 'sentinel-api-key-not-real',
provider: 'generic-chat-completion-api',
model: 'sentinel-model',
};
const serverA = startOpenAICompatProxyServer({
profile,
port: 0,
authToken: 'sentinel-local-token-a',
});
const serverB = startOpenAICompatProxyServer({
profile,
port: 0,
authToken: 'sentinel-local-token-b',
});
await Promise.all([
new Promise<void>((resolve) => serverA.once('listening', resolve)),
new Promise<void>((resolve) => serverB.once('listening', resolve)),
]);
await closeOpenAICompatProxyServer(serverA, 50);
expect(serverB.listening).toBe(true);
expect(globalThis.fetch).toBe(originalFetch);
expect(getGlobalDispatcher()).toBe(originalDispatcher);
await closeOpenAICompatProxyServer(serverB, 50);
expect(globalThis.fetch).toBe(originalFetch);
expect(getGlobalDispatcher()).toBe(originalDispatcher);
});
});
+74 -4
View File
@@ -9,8 +9,13 @@ import { resolveOpenAICompatProfileConfig } from '../../../src/proxy/profile-rou
import {
attachDisconnectAbortHandlers,
buildUpstreamAgentTimeouts,
handleProxyMessagesRequest,
handleProxyMessagesRequest as handleProxyMessagesRequestImpl,
} from '../../../src/proxy/server/messages-route';
import {
closeUpstreamDispatcher,
isBunRuntime,
toLogErrorInfo,
} from '../../../src/proxy/server/upstream-transport';
import { loadSettings } from '../../../src/utils/config-manager';
class FakeSocket extends EventEmitter {
@@ -70,6 +75,24 @@ function buildProfile(profileName: string) {
return profile!;
}
function handleProxyMessagesRequest(
req: Parameters<typeof handleProxyMessagesRequestImpl>[0],
res: Parameters<typeof handleProxyMessagesRequestImpl>[1],
profile: Parameters<typeof handleProxyMessagesRequestImpl>[2],
expectedAuthToken: string,
insecureDispatcher?: Parameters<typeof handleProxyMessagesRequestImpl>[4]
) {
return handleProxyMessagesRequestImpl(
req,
res,
profile,
expectedAuthToken,
insecureDispatcher,
undefined,
globalThis.fetch
);
}
beforeEach(() => {
originalCcsHome = process.env.CCS_HOME;
originalFetch = globalThis.fetch;
@@ -191,6 +214,45 @@ describe('buildUpstreamAgentTimeouts', () => {
});
});
describe('closeUpstreamDispatcher', () => {
it('closes an owned dispatcher once across repeated cleanup', async () => {
let closeCalls = 0;
const dispatcher = {
close: async () => {
closeCalls += 1;
},
} as never;
await Promise.all([
closeUpstreamDispatcher(dispatcher),
closeUpstreamDispatcher(dispatcher),
closeUpstreamDispatcher(dispatcher),
]);
expect(closeCalls).toBe(1);
});
});
describe('toLogErrorInfo', () => {
it('preserves nested transport cause metadata for diagnostics', () => {
const cause = Object.assign(new Error('invalid onError method'), {
name: 'InvalidArgumentError',
code: 'UND_ERR_INVALID_ARG',
});
const error = new TypeError('fetch failed', { cause });
expect(toLogErrorInfo(error)).toEqual({
name: 'TypeError',
message: 'fetch failed',
cause: {
name: 'InvalidArgumentError',
message: 'invalid onError method',
code: 'UND_ERR_INVALID_ARG',
},
});
});
});
describe('handleProxyMessagesRequest', () => {
it('dispatches secure upstream fetches with an explicit dispatcher (not undici defaults)', async () => {
const activeProfile = buildProfile('hf');
@@ -217,7 +279,11 @@ describe('handleProxyMessagesRequest', () => {
// A bare global-dispatcher fallback would reapply undici's 300s
// headersTimeout/bodyTimeout and undercut the proxy's request timeout.
expect(capturedDispatcher).toBeDefined();
if (isBunRuntime()) {
expect(capturedDispatcher).toBeUndefined();
} else {
expect(capturedDispatcher).toBeDefined();
}
expect(res.statusCode).toBe(502);
});
@@ -403,9 +469,13 @@ describe('handleProxyMessagesRequest', () => {
);
await pending;
expect(capturedDispatcher).toBeInstanceOf(Agent);
if (isBunRuntime()) {
expect(capturedDispatcher).toBeUndefined();
} else {
expect(capturedDispatcher).toBeInstanceOf(Agent);
}
expect(capturedDispatcher).not.toBe(sharedDispatcher);
expect(closeCalls).toBe(1);
expect(closeCalls).toBe(isBunRuntime() ? 0 : 1);
expect(res.statusCode).toBe(502);
});
@@ -63,7 +63,9 @@ beforeEach(() => {
baselineUncaughtExceptionListeners = process.listeners('uncaughtException');
baselineUnhandledRejectionListeners = process.listeners('unhandledRejection');
mock.module('../../../../src/utils/fetch-proxy-setup', () => ({}));
mock.module('../../../../src/utils/fetch-proxy-setup', () => ({
applyGlobalFetchProxy: () => ({ enabled: false }),
}));
mock.module('../../../../src/utils/error-manager', () => ({
ErrorManager: class ErrorManager {
static async showProfileNotFound(): Promise<void> {}
@@ -1,5 +1,9 @@
import { describe, expect, it } from 'bun:test';
import { redactContext } from '../../../../src/services/logging/log-redaction';
import {
maskSecretTokens,
redactContext,
redactErrorInfo,
} from '../../../../src/services/logging/log-redaction';
describe('log redaction', () => {
it('redacts sensitive keys and preserves non-sensitive values', () => {
@@ -100,4 +104,31 @@ describe('log redaction', () => {
},
});
});
it('redacts proxy URL userinfo from nested error causes', () => {
const redacted = redactErrorInfo({
name: 'TypeError',
message: 'fetch failed',
cause: {
name: 'ProxyError',
code: 'ECONNREFUSED',
message: 'connect http://sentinel-user:sentinel-password@proxy.example:8080',
},
});
expect(redacted?.cause).toEqual({
name: 'ProxyError',
code: 'ECONNREFUSED',
message: 'connect http://[redacted]@proxy.example:8080',
});
expect(JSON.stringify(redacted)).not.toContain('sentinel-user');
expect(JSON.stringify(redacted)).not.toContain('sentinel-password');
});
it('redacts URL userinfo without changing non-credential URLs', () => {
expect(maskSecretTokens('https://user:pass@example.com/path')).toBe(
'https://[redacted]@example.com/path'
);
expect(maskSecretTokens('https://example.com/path')).toBe('https://example.com/path');
});
});
+25 -19
View File
@@ -4,7 +4,7 @@ import {
Agent,
Dispatcher,
ProxyAgent,
fetch,
fetch as undiciFetch,
getGlobalDispatcher,
setGlobalDispatcher,
} from 'undici';
@@ -26,18 +26,24 @@ const PROXY_ENV_KEYS = [
describe('global fetch proxy setup', () => {
const originalEnv = new Map<string, string | undefined>();
const originalFetch = globalThis.fetch;
const originalDispatcher = getGlobalDispatcher();
beforeEach(() => {
globalThis.fetch = originalFetch;
setGlobalDispatcher(originalDispatcher);
for (const key of PROXY_ENV_KEYS) {
originalEnv.set(key, process.env[key]);
delete process.env[key];
}
setGlobalDispatcher(originalDispatcher);
});
afterEach(() => {
afterEach(async () => {
const activeDispatcher = getGlobalDispatcher();
globalThis.fetch = originalFetch;
setGlobalDispatcher(originalDispatcher);
if (activeDispatcher !== originalDispatcher) {
await activeDispatcher.close();
}
for (const key of PROXY_ENV_KEYS) {
const value = originalEnv.get(key);
if (value === undefined) {
@@ -112,18 +118,6 @@ describe('global fetch proxy setup', () => {
});
});
it('rebinds globalThis.fetch to undici fetch when proxying is enabled', () => {
const originalFetch = globalThis.fetch;
process.env.HTTP_PROXY = 'http://proxy.example:8080';
try {
expect(applyGlobalFetchProxy()).toEqual({ enabled: true });
expect(globalThis.fetch).toBe(fetch);
} finally {
globalThis.fetch = originalFetch;
}
});
it('bypasses loopback fetches even when HTTP_PROXY is set', async () => {
process.env.HTTP_PROXY = 'http://127.0.0.1:9';
@@ -139,11 +133,14 @@ describe('global fetch proxy setup', () => {
}
try {
expect(applyGlobalFetchProxy()).toEqual({ enabled: true });
const response = await fetch(`http://127.0.0.1:${address.port}/health`);
const dispatcher = createGlobalFetchProxyDispatcher();
expect(dispatcher).not.toBeNull();
const response = await undiciFetch(`http://127.0.0.1:${address.port}/health`, {
dispatcher: dispatcher!,
});
expect(response.status).toBe(200);
expect(await response.text()).toBe('ok');
await dispatcher?.close();
} finally {
await new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
@@ -158,6 +155,15 @@ describe('global fetch proxy setup', () => {
expect(result).toEqual({ directCalls: 0, proxyCalls: 1 });
});
it('applies the proxy transport only when explicitly invoked', () => {
process.env.HTTP_PROXY = 'http://proxy.example:8080';
expect(globalThis.fetch).toBe(originalFetch);
expect(applyGlobalFetchProxy()).toEqual({ enabled: true });
expect(globalThis.fetch).toBe(undiciFetch);
expect(getGlobalDispatcher()).not.toBe(originalDispatcher);
});
it('routes HTTPS requests through HTTPS_PROXY', async () => {
process.env.HTTPS_PROXY = 'http://proxy.example:8443';