fix(proxy): restore direct fetch routing when clearing proxies (#2406)

* fix(proxy): restore direct fetch routing when clearing proxies

* fix(proxy): preserve dispatcher ownership and lazy setup

* fix(proxy): retire invalidated global TLS dispatchers

* fix(proxy): preserve dispatchers retained by fetch callers
This commit is contained in:
JATMN
2026-10-24 15:27:03 -07:00
committed by GitHub
parent be3a71711c
commit 6712d5a78c
3 changed files with 307 additions and 53 deletions
+256 -36
View File
@@ -14,6 +14,7 @@ const run = promisify(execFile)
const { key, cert } = createTestTlsFixture()
let directory: string
let modulePath: string
let proxyModulePath: string
beforeAll(async () => {
directory = await mkdtemp(join(tmpdir(), 'proxy-no-proxy-'))
@@ -23,15 +24,16 @@ beforeAll(async () => {
writeFile(join(directory, 'client.key'), key),
])
modulePath = join(directory, 'network.mjs')
proxyModulePath = join(directory, 'proxy.mjs')
const entrypoint = join(directory, 'entry.ts')
await writeFile(entrypoint, `
export { getProxyAgent, getNoProxy, shouldBypassProxy, configureGlobalAgents, clearProxyCache } from ${JSON.stringify(join(import.meta.dir, 'proxy.ts'))};
export { getProxyAgent, getProxyFetchOptions, getNoProxy, shouldBypassProxy, configureGlobalAgents, clearProxyCache } from ${JSON.stringify(join(import.meta.dir, 'proxy.ts'))};
export { StructuredIO } from ${JSON.stringify(join(import.meta.dir, '../cli/structuredIO.ts'))};
export { getCACertificates } from ${JSON.stringify(join(import.meta.dir, 'caCerts.ts'))};
export { getMTLSConfig, getMTLSAgent } from ${JSON.stringify(join(import.meta.dir, 'mtls.ts'))};
export { clearCACertsCache, getCACertificates } from ${JSON.stringify(join(import.meta.dir, 'caCerts.ts'))};
export { clearMTLSCache, getMTLSConfig, getMTLSAgent } from ${JSON.stringify(join(import.meta.dir, 'mtls.ts'))};
`)
const result = await Bun.build({
entrypoints: [entrypoint],
entrypoints: [entrypoint, join(import.meta.dir, 'proxy.ts')],
target: 'node',
plugins: [{
name: 'node-proxy-environment-fixture',
@@ -71,6 +73,7 @@ beforeAll(async () => {
})
if (!result.success) throw new AggregateError(result.logs, 'Could not build proxy environment fixture')
await writeFile(modulePath, await result.outputs[0].text())
await writeFile(proxyModulePath, await result.outputs[1].text())
})
afterAll(async () => {
@@ -80,6 +83,8 @@ afterAll(async () => {
test.each([
'combined certificate and no_proxy',
'certificate-only',
'proxy-clear-ca',
'proxy-clear-mtls',
] as const)('structured I/O refreshes certificate caches with a %s update', async mode => {
const caPath = join(directory, 'ca.pem')
const certPath = join(directory, 'client.pem')
@@ -89,7 +94,7 @@ test.each([
let requests = 0
let authorizedRequests = 0
const target = createHttpsServer({
key, cert, ca: cert, requestCert: true, rejectUnauthorized: true,
key, cert, ca: cert, requestCert: mode !== 'proxy-clear-ca', rejectUnauthorized: true,
}, (request, response) => {
requests++
if ('authorized' in request.socket && request.socket.authorized) authorizedRequests++
@@ -150,6 +155,7 @@ test.each([
const previousGlobalDispatcher = getGlobalDispatcher();
network.configureGlobalAgents();
const agents = new Set([cached]);
const tlsAgents = new Set();
const update = async variables => {
const io = new network.StructuredIO((async function* () {
yield JSON.stringify({ type: 'update_environment_variables', variables }) + '\\n';
@@ -161,38 +167,53 @@ test.each([
try {
await update({
NODE_EXTRA_CA_CERTS: config.caPath,
CLAUDE_CODE_CLIENT_CERT: config.certPath,
CLAUDE_CODE_CLIENT_KEY: config.keyPath,
...(config.mode !== 'proxy-clear-ca' && {
CLAUDE_CODE_CLIENT_CERT: config.certPath,
CLAUDE_CODE_CLIENT_KEY: config.keyPath,
}),
...(config.mode !== 'certificate-only' && { no_proxy: 'unused.invalid' }),
});
agents.add(getGlobalDispatcher());
const expectedCert = readFileSync(config.certPath, 'utf8');
const expectedKey = readFileSync(config.keyPath, 'utf8');
assert.deepEqual(network.getMTLSConfig(), { cert: expectedCert, key: expectedKey });
assert.ok(network.getCACertificates().includes(readFileSync(config.caPath, 'utf8')));
assert.equal(network.getMTLSAgent().options.cert, expectedCert);
assert.equal(network.getMTLSAgent().options.key, expectedKey);
for (const noProxy of ['unused.invalid', '*.127.0.0.1']) {
if (network.getNoProxy() !== noProxy) await update({ no_proxy: noProxy });
const dispatcher = network.getProxyAgent(config.proxy);
agents.add(dispatcher);
assert.notEqual(dispatcher, cached);
for (const options of [{ dispatcher }, {}]) {
const response = await fetch(config.url, options);
assert.equal(response.status, 200);
assert.equal(await response.text(), 'authenticated response');
tlsAgents.add(network.getMTLSAgent());
if (config.mode.startsWith('proxy-clear')) {
const proxied = await fetch(config.url);
assert.equal(await proxied.text(), 'authenticated response');
await update({ HTTPS_PROXY: '' });
agents.add(getGlobalDispatcher());
tlsAgents.add(network.getMTLSAgent());
const direct = await fetch(config.url);
assert.equal(direct.status, 200);
assert.equal(await direct.text(), 'authenticated response');
} else {
const expectedCert = readFileSync(config.certPath, 'utf8');
const expectedKey = readFileSync(config.keyPath, 'utf8');
assert.deepEqual(network.getMTLSConfig(), { cert: expectedCert, key: expectedKey });
assert.ok(network.getCACertificates().includes(readFileSync(config.caPath, 'utf8')));
assert.equal(network.getMTLSAgent().options.cert, expectedCert);
assert.equal(network.getMTLSAgent().options.key, expectedKey);
for (const noProxy of ['unused.invalid', '*.127.0.0.1']) {
if (network.getNoProxy() !== noProxy) await update({ no_proxy: noProxy });
const dispatcher = network.getProxyAgent(config.proxy);
agents.add(dispatcher);
assert.notEqual(dispatcher, cached);
for (const options of [{ dispatcher }, {}]) {
const response = await fetch(config.url, options);
assert.equal(response.status, 200);
assert.equal(await response.text(), 'authenticated response');
}
}
}
console.log('verified');
} finally {
setGlobalDispatcher(previousGlobalDispatcher);
await Promise.all([...agents].map(dispatcher => dispatcher.close()));
await Promise.all([...agents].map(dispatcher => dispatcher.destroy()));
for (const agent of tlsAgents) agent.destroy();
}
`, modulePath, JSON.stringify(config)], { env, timeout: 6000 })
expect(stdout.trim()).toBe('verified')
expect(connects).toBe(2)
expect(requests).toBe(4)
expect(authorizedRequests).toBe(4)
expect(connects).toBe(mode.startsWith('proxy-clear') ? 1 : 2)
expect(requests).toBe(mode.startsWith('proxy-clear') ? 2 : 4)
expect(authorizedRequests).toBe(mode === 'proxy-clear-ca' ? 0 : mode === 'proxy-clear-mtls' ? 2 : 4)
} finally {
for (const socket of sockets) socket.destroy()
await Promise.all([target, proxy].map(server => new Promise<void>(resolve => {
@@ -262,7 +283,7 @@ test.each(['TLS/proxy settings', 'unrelated session token'] as const)(
console.log('verified');
} finally {
setGlobalDispatcher(previousGlobalDispatcher);
await Promise.all([...dispatchers].map(dispatcher => dispatcher.close()));
await Promise.all([...dispatchers].map(dispatcher => dispatcher.destroy()));
for (const agent of tlsAgents) agent.destroy();
}
`, modulePath, JSON.stringify({ mode, directory })], { env, timeout: 6000 })
@@ -274,6 +295,7 @@ for (const mode of [
'lowercase-bypass', 'lowercase-proxy', 'empty-lowercase',
'refresh-uppercase', 'refresh-lowercase', 'cache-wildcard',
'update-uppercase', 'update-lowercase', 'clear-cache',
'clear-proxy', 'clear-unset', 'retained-proxy',
] as const) {
// The Node child has a 6s deadline; allow extra time for cleanup.
test(`Node NO_PROXY routing: ${mode}`, async () => {
@@ -285,14 +307,17 @@ for (const mode of [
response.end('direct')
})
const proxy = createServer()
proxy.on('connect', (_request, client) => {
const replacementProxy = mode === 'retained-proxy' ? createServer() : undefined
const proxies = [proxy, ...(replacementProxy ? [replacementProxy] : [])]
for (const server of proxies) server.on('connect', (_request, client) => {
connects++
client.write('HTTP/1.1 200 Connection Established\r\n\r\n')
client.once('data', () => {
client.end('HTTP/1.1 200 OK\r\nContent-Length: 5\r\nConnection: close\r\n\r\nproxy')
})
})
for (const server of [target, proxy]) {
const servers = [target, ...proxies]
for (const server of servers) {
server.on('connection', socket => {
sockets.add(socket)
socket.on('close', () => sockets.delete(socket))
@@ -304,6 +329,10 @@ for (const mode of [
await once(target, 'listening')
proxy.listen(0, '127.0.0.1')
await once(proxy, 'listening')
if (replacementProxy) {
replacementProxy.listen(0, '127.0.0.1')
await once(replacementProxy, 'listening')
}
const env = { ...process.env }
for (const key of [
'HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy', 'NO_PROXY', 'no_proxy',
@@ -313,14 +342,19 @@ for (const mode of [
const config = {
mode,
proxy: `http://127.0.0.1:${(proxy.address() as AddressInfo).port}`,
replacementProxy: replacementProxy
? `http://127.0.0.1:${(replacementProxy.address() as AddressInfo).port}`
: undefined,
url: `http://127.0.0.1:${(target.address() as AddressInfo).port}/`,
}
const { stdout } = await run('node', ['--input-type=module', '-e', `
import assert from 'node:assert/strict';
import { pathToFileURL } from 'node:url';
const network = await import(pathToFileURL(process.argv[1]));
const { mode, proxy, url } = JSON.parse(process.argv[2]);
process.env.HTTPS_PROXY = proxy;
const { getGlobalDispatcher, setGlobalDispatcher } = await import(pathToFileURL(${JSON.stringify(Bun.resolveSync('undici/index.js', import.meta.dir))}));
const { mode, proxy, replacementProxy, url } = JSON.parse(process.argv[2]);
const previousGlobalDispatcher = getGlobalDispatcher();
if (mode !== 'clear-unset') process.env.HTTPS_PROXY = proxy;
const agents = new Set();
const agent = () => {
const result = network.getProxyAgent(proxy);
@@ -341,7 +375,50 @@ for (const mode of [
}
};
try {
if (mode.startsWith('lowercase') || mode === 'empty-lowercase') {
if (mode === 'retained-proxy') {
const retained = network.getProxyFetchOptions().dispatcher;
agents.add(retained);
network.configureGlobalAgents();
assert.equal(getGlobalDispatcher(), retained);
await request('proxy');
await update({ HTTPS_PROXY: replacementProxy });
agents.add(getGlobalDispatcher());
assert.notEqual(getGlobalDispatcher(), retained);
await request('proxy');
await request('proxy', retained);
await update({ HTTPS_PROXY: '' });
agents.add(getGlobalDispatcher());
await request('direct');
await request('proxy', retained);
} else if (mode === 'clear-proxy' || mode === 'clear-unset') {
if (mode === 'clear-proxy') {
network.configureGlobalAgents();
const installed = agent();
let closed = 0;
const close = installed.close.bind(installed);
installed.close = () => { closed++; return close(); };
network.configureGlobalAgents();
assert.equal(closed, 0, 'the active cached dispatcher must stay open');
await request('proxy');
await update({ HTTPS_PROXY: '' });
agents.add(getGlobalDispatcher());
await request('direct');
assert.equal(closed, 0, 'retained proxy dispatchers must stay open');
// Reusing the same proxy must use the refreshed cache.
process.env.HTTPS_PROXY = proxy;
network.configureGlobalAgents();
agents.add(getGlobalDispatcher());
await request('proxy');
await update({ HTTPS_PROXY: '' });
} else {
await update({ HTTPS_PROXY: '' });
}
agents.add(getGlobalDispatcher());
await request('direct');
await update({ HTTPS_PROXY: '' });
agents.add(getGlobalDispatcher());
await request('direct');
} else if (mode.startsWith('lowercase') || mode === 'empty-lowercase') {
process.env.NO_PROXY = mode === 'lowercase-bypass' ? 'unused.invalid' : '127.0.0.1';
process.env.no_proxy = mode === 'lowercase-bypass' ? '127.0.0.1' :
mode === 'empty-lowercase' ? '' : 'unused.invalid';
@@ -391,22 +468,165 @@ for (const mode of [
}
console.log('verified');
} finally {
for (const dispatcher of agents) await dispatcher.close();
setGlobalDispatcher(previousGlobalDispatcher);
for (const dispatcher of agents) await dispatcher.destroy();
}
`, modulePath, JSON.stringify(config)], { env, timeout: 6000 })
expect(stdout.trim()).toBe('verified')
const expectedConnects = mode === 'lowercase-bypass' ? 0 :
const expectedConnects = mode === 'retained-proxy' ? 4 : mode === 'clear-proxy' ? 2 : mode === 'clear-unset' ? 0 : mode === 'lowercase-bypass' ? 0 :
mode.startsWith('refresh') ? 3 : mode.startsWith('update') || mode === 'clear-cache' ? 2 : 1
const expectedDirect = mode === 'lowercase-bypass' ? 1 :
const expectedDirect = mode === 'retained-proxy' ? 1 : mode === 'clear-proxy' ? 3 : mode === 'clear-unset' ? 2 : mode === 'lowercase-bypass' ? 1 :
mode.startsWith('lowercase') || mode === 'empty-lowercase' ? 0 :
mode.startsWith('update') || mode === 'clear-cache' ? 4 : mode === 'cache-wildcard' ? 3 : 2
expect(connects).toBe(expectedConnects)
expect(requests).toBe(expectedDirect)
} finally {
for (const socket of sockets) socket.destroy()
await Promise.all([target, proxy].map(server => new Promise<void>(resolve => {
await Promise.all(servers.map(server => new Promise<void>(resolve => {
server.close(() => resolve())
})))
}
}, 10000)
}
test('empty configuration does not load or install a global dispatcher', async () => {
const env = { ...process.env }
for (const name of [
'HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy', 'NO_PROXY', 'no_proxy',
'NODE_OPTIONS', 'NODE_EXTRA_CA_CERTS', 'CLAUDE_CODE_CLIENT_CERT',
'CLAUDE_CODE_CLIENT_KEY', 'CLAUDE_CODE_CLIENT_KEY_PASSPHRASE',
]) delete env[name]
const { stdout } = await run('node', ['--input-type=module', '-e', `
import assert from 'node:assert/strict';
import Module from 'node:module';
import { pathToFileURL } from 'node:url';
const load = Module._load;
Module._load = function(specifier, ...args) {
assert.ok(!specifier.includes('undici'), 'empty configuration must stay lazy');
return load.call(this, specifier, ...args);
};
const symbols = ['undici.globalDispatcher.1', 'undici.globalDispatcher.2'].map(Symbol.for);
try {
const network = await import(pathToFileURL(process.argv[1]));
// Importing HTTP clients may initialize Node's built-in fetch dispatcher.
const before = symbols.map(symbol => globalThis[symbol]);
network.configureGlobalAgents();
network.configureGlobalAgents();
assert.deepEqual(symbols.map(symbol => globalThis[symbol]), before);
// The full fixture imports unrelated services which can eagerly load
// dispatchers. Only the environment-update operation must stay lazy.
Module._load = load;
const structured = await import(pathToFileURL(process.argv[2]));
const structuredBefore = symbols.map(symbol => globalThis[symbol]);
Module._load = function(specifier, ...args) {
assert.ok(!specifier.includes('undici'), 'empty update must stay lazy');
return load.call(this, specifier, ...args);
};
const io = new structured.StructuredIO((async function* () {
yield JSON.stringify({ type: 'update_environment_variables', variables: { HTTPS_PROXY: '' } }) + '\\n';
})());
for await (const message of io.structuredInput) assert.fail(message.type);
assert.deepEqual(symbols.map(symbol => globalThis[symbol]), structuredBefore);
console.log('verified');
} finally { Module._load = load; }
`, proxyModulePath, modulePath], { env, timeout: 6000 })
expect(stdout.trim()).toBe('verified')
}, 10000)
test.each(['shared TLS', 'TLS clear', 'TLS refresh', 'proxy replacement', 'external replacement'] as const)(
'global dispatcher ownership: %s', async mode => {
const env = { ...process.env }
for (const name of [
'HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy', 'NO_PROXY', 'no_proxy',
'NODE_OPTIONS', 'NODE_EXTRA_CA_CERTS', 'CLAUDE_CODE_CLIENT_CERT',
'CLAUDE_CODE_CLIENT_KEY', 'CLAUDE_CODE_CLIENT_KEY_PASSPHRASE',
]) delete env[name]
const { stdout } = await run('node', ['--input-type=module', '-e', `
import assert from 'node:assert/strict';
import { pathToFileURL } from 'node:url';
const undici = (await import(pathToFileURL(${JSON.stringify(Bun.resolveSync('undici/index.js', import.meta.dir))}))).default;
const network = await import(pathToFileURL(process.argv[1]));
const { mode, directory } = JSON.parse(process.argv[2]);
const original = undici.getGlobalDispatcher();
const setGlobalDispatcher = undici.setGlobalDispatcher;
const dispatchers = new Set();
undici.setGlobalDispatcher = dispatcher => {
dispatchers.add(dispatcher);
setGlobalDispatcher(dispatcher);
};
const countCloses = dispatcher => {
const close = dispatcher.close;
let count = 0;
dispatcher.close = function(...args) { count++; return close.apply(this, args); };
return () => count;
};
const defaultCloses = countCloses(original);
try {
network.configureGlobalAgents();
assert.equal(undici.getGlobalDispatcher(), original);
if (mode === 'shared TLS' || mode.startsWith('TLS')) {
process.env.NODE_EXTRA_CA_CERTS = directory + '/ca.pem';
network.clearCACertsCache();
network.clearMTLSCache();
network.configureGlobalAgents();
const shared = undici.getGlobalDispatcher();
assert.notEqual(shared, original, 'CA configuration must install a TLS dispatcher');
const sharedCloses = countCloses(shared);
network.configureGlobalAgents();
assert.equal(undici.getGlobalDispatcher(), shared, 'cached TLS dispatcher must be reused');
assert.equal(sharedCloses(), 0, 'cached TLS dispatcher must stay open');
if (mode.startsWith('TLS')) {
if (mode === 'TLS clear') delete process.env.NODE_EXTRA_CA_CERTS;
network.clearCACertsCache();
network.clearMTLSCache();
network.configureGlobalAgents();
const replacement = undici.getGlobalDispatcher();
assert.notEqual(replacement, shared, 'invalidated TLS configuration must be replaced');
if (mode === 'TLS clear') assert.ok(replacement instanceof undici.Agent);
assert.equal(sharedCloses(), 1, 'retired TLS dispatcher must be closed after cache invalidation');
network.configureGlobalAgents();
if (mode === 'TLS clear') assert.equal(undici.getGlobalDispatcher(), replacement);
} else {
process.env.HTTPS_PROXY = 'http://127.0.0.1:1';
network.configureGlobalAgents();
delete process.env.HTTPS_PROXY;
network.clearProxyCache();
network.configureGlobalAgents();
assert.equal(sharedCloses(), 0, 'retained cached TLS dispatcher must stay open');
}
} else {
process.env.HTTPS_PROXY = 'http://127.0.0.1:1';
network.configureGlobalAgents();
const first = undici.getGlobalDispatcher();
const firstCloses = countCloses(first);
if (mode === 'external replacement') {
const external = new undici.Agent();
undici.setGlobalDispatcher(external);
delete process.env.HTTPS_PROXY;
network.configureGlobalAgents();
assert.equal(undici.getGlobalDispatcher(), external);
assert.equal(firstCloses(), 0, 'a cached proxy must stay open');
} else {
process.env.HTTPS_PROXY = 'http://127.0.0.1:2';
network.configureGlobalAgents();
const second = undici.getGlobalDispatcher();
assert.equal(firstCloses(), 0, 'replaced shared proxy must stay open');
assert.notEqual(second, first);
assert.equal(network.getProxyAgent(process.env.HTTPS_PROXY), second);
const recreated = network.getProxyAgent('http://127.0.0.1:1');
dispatchers.add(recreated);
assert.notEqual(recreated, first, 'retired proxy must leave the cache');
}
}
assert.equal(defaultCloses(), 0, 'the default dispatcher must stay open');
console.log('verified');
} finally {
undici.setGlobalDispatcher = setGlobalDispatcher;
setGlobalDispatcher(original);
await Promise.all([...dispatchers].map(dispatcher => dispatcher.destroy()));
network.getMTLSAgent()?.destroy();
}
`, modulePath, JSON.stringify({ mode, directory })], { env, timeout: 6000 })
expect(stdout.trim()).toBe('verified')
}, 10000,
)
+3 -1
View File
@@ -132,9 +132,11 @@ describe('runtime CA certificates through CONNECT', () => {
clearMTLSCache()
clearProxyCache()
configureGlobalAgents() // Eject the test's global axios interceptor.
const directDispatcher = getGlobalDispatcher()
Object.assign(axios.defaults, defaults)
setGlobalDispatcher(dispatcher)
if (testDispatcher !== dispatcher) await testDispatcher.close?.()
if (testDispatcher !== dispatcher) await testDispatcher.destroy?.()
if (directDispatcher !== dispatcher) await directDispatcher.destroy?.()
restoreEnv()
clearCACertsCache()
clearMTLSCache()
+48 -16
View File
@@ -258,6 +258,10 @@ export function createAxiosInstance(
return instance
}
function getProxyAgentCacheKey(uri: string): string {
return JSON.stringify([uri, getNoProxy()])
}
/**
* Get or create a proxy agent cached by URI and effective NO_PROXY value.
* Include the bypass list because wildcard normalization pins it on the agent.
@@ -306,7 +310,7 @@ export const getProxyAgent = memoize((uri: string): undici.Dispatcher => {
return typeof Bun === 'undefined'
? new undiciMod.Dispatcher1Wrapper(agent)
: agent
}, (uri: string) => JSON.stringify([uri, getNoProxy()]))
}, getProxyAgentCacheKey)
/**
* Get an HTTP agent configured for WebSocket proxy support
@@ -395,6 +399,11 @@ export function getProxyFetchOptions(opts?: { forAnthropicAPI?: boolean }): {
* This ensures all HTTP requests use the proxy and/or mTLS if configured
*/
let proxyInterceptorId: number | undefined
let installedDispatcher: undici.Dispatcher | undefined
let installedProxyCacheKey: string | undefined
let installedTLSDispatcher = false
let installedTLSAgent: ReturnType<typeof getMTLSAgent>
let ownsInstalledDispatcher = false
export function configureGlobalAgents(): void {
const proxyUrl = getProxyUrl()
@@ -438,25 +447,48 @@ export function configureGlobalAgents(): void {
}
return config
})
// Set global dispatcher that now respects NO_PROXY via EnvHttpProxyAgent
// eslint-disable-next-line @typescript-eslint/no-require-imports
;(require('undici') as typeof undici).setGlobalDispatcher(
getProxyAgent(proxyUrl),
)
} else if (mtlsAgent) {
// No proxy but mTLS is configured
} else {
axios.defaults.httpsAgent = mtlsAgent
}
// Set undici global dispatcher with mTLS
const mtlsOptions = getTLSFetchOptions()
if (mtlsOptions.dispatcher) {
// eslint-disable-next-line @typescript-eslint/no-require-imports
;(require('undici') as typeof undici).setGlobalDispatcher(
mtlsOptions.dispatcher,
)
// Keep the ordinary direct path lazy. Only clear our proxy/TLS installation
// while it is still global; another caller may have installed a replacement.
if (!proxyUrl && !mtlsAgent && installedProxyCacheKey === undefined && !installedTLSDispatcher) return
// eslint-disable-next-line @typescript-eslint/no-require-imports
const undiciMod = require('undici') as typeof undici
if (!proxyUrl && !mtlsAgent && undiciMod.getGlobalDispatcher() !== installedDispatcher) return
// Reuse our TLS installation while its source configuration remains cached.
const cachedTLSDispatcher = !proxyUrl && mtlsAgent && installedTLSDispatcher &&
installedTLSAgent === mtlsAgent ? installedDispatcher : undefined
const tlsOptions = proxyUrl || cachedTLSDispatcher ? undefined : getTLSFetchOptions()
const dispatcher = proxyUrl ? getProxyAgent(proxyUrl) :
cachedTLSDispatcher ?? tlsOptions?.dispatcher ?? new undiciMod.Agent({ connect: tlsOptions?.tls })
undiciMod.setGlobalDispatcher(dispatcher)
if (installedDispatcher && installedDispatcher !== dispatcher) {
// Evict only the retired proxy, leaving the active dispatcher cached.
if (installedProxyCacheKey !== undefined &&
getProxyAgent.cache.get(installedProxyCacheKey) === installedDispatcher) {
getProxyAgent.cache.delete(installedProxyCacheKey)
}
// Proxy dispatchers are shared with callers that can retain fetch options.
// Eviction does not prove those references are gone, so leave them open.
if (ownsInstalledDispatcher || (installedTLSDispatcher && installedTLSAgent !== mtlsAgent)) {
try {
// Graceful close lets in-flight requests finish without blocking updates.
void installedDispatcher.close().catch(() => {})
} catch {
// Dispatcher cleanup is best effort.
}
}
}
installedDispatcher = dispatcher
installedProxyCacheKey = proxyUrl ? getProxyAgentCacheKey(proxyUrl) : undefined
installedTLSDispatcher = !proxyUrl && !!mtlsAgent
installedTLSAgent = mtlsAgent
// Only the plain direct dispatcher is private; proxy options are handed out.
ownsInstalledDispatcher = !proxyUrl && !mtlsAgent
}
/**