From 6807da544bf62eaa6c996fe3e0ab948cbf4c616d Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 5 May 2026 18:05:50 -0700 Subject: [PATCH] fix(net): preserve no-proxy undici stream timeouts --- .../net/undici-global-dispatcher.test.ts | 34 ++++++++++++++----- src/infra/net/undici-global-dispatcher.ts | 33 ++++++++++-------- 2 files changed, 43 insertions(+), 24 deletions(-) diff --git a/src/infra/net/undici-global-dispatcher.test.ts b/src/infra/net/undici-global-dispatcher.test.ts index 01db4cc5f00..cecc34a1ce6 100644 --- a/src/infra/net/undici-global-dispatcher.test.ts +++ b/src/infra/net/undici-global-dispatcher.test.ts @@ -106,24 +106,32 @@ describe("ensureGlobalUndiciStreamTimeouts", () => { vi.mocked(resolveEnvHttpProxyAgentOptions).mockReturnValue(undefined); }); - it("records timeout bridge without importing undici when no env proxy is configured", () => { + it("replaces direct Agent dispatcher with extended stream timeouts when no env proxy is configured", () => { getDefaultAutoSelectFamily.mockReturnValue(true); ensureGlobalUndiciStreamTimeouts(); - expect(loadUndiciGlobalDispatcherDeps).not.toHaveBeenCalled(); - expect(setGlobalDispatcher).not.toHaveBeenCalled(); + expect(loadUndiciGlobalDispatcherDeps).toHaveBeenCalledTimes(1); + expect(setGlobalDispatcher).toHaveBeenCalledTimes(1); + const next = getCurrentDispatcher() as { options?: Record }; + expect(next).toBeInstanceOf(Agent); + expect(next.options?.bodyTimeout).toBe(DEFAULT_UNDICI_STREAM_TIMEOUT_MS); + expect(next.options?.headersTimeout).toBe(DEFAULT_UNDICI_STREAM_TIMEOUT_MS); + expect(next.options?.connect).toEqual({ + autoSelectFamily: true, + autoSelectFamilyAttemptTimeout: 300, + }); expect(undiciGlobalDispatcherModule._globalUndiciStreamTimeoutMs).toBe( DEFAULT_UNDICI_STREAM_TIMEOUT_MS, ); }); - it("does not initialize the undici global dispatcher in a no-proxy subprocess", () => { + it("does not initialize the undici global dispatcher during no-proxy bootstrap", () => { const moduleUrl = pathToFileURL(path.resolve("src/infra/net/undici-global-dispatcher.ts")).href; const source = ` const dispatcherKey = Symbol.for("undici.globalDispatcher.1"); const mod = await import(${JSON.stringify(moduleUrl)}); - mod.ensureGlobalUndiciStreamTimeouts({ timeoutMs: 1_900_000 }); + mod.ensureGlobalUndiciEnvProxyDispatcher(); if (globalThis[dispatcherKey] !== undefined) { throw new Error("undici global dispatcher was initialized"); } @@ -214,8 +222,12 @@ describe("ensureGlobalUndiciStreamTimeouts", () => { it("does not lower global stream timeouts below the default floor", () => { ensureGlobalUndiciStreamTimeouts({ timeoutMs: 15_000 }); - expect(loadUndiciGlobalDispatcherDeps).not.toHaveBeenCalled(); - expect(setGlobalDispatcher).not.toHaveBeenCalled(); + expect(loadUndiciGlobalDispatcherDeps).toHaveBeenCalledTimes(1); + expect(setGlobalDispatcher).toHaveBeenCalledTimes(1); + const next = getCurrentDispatcher() as { options?: Record }; + expect(next).toBeInstanceOf(Agent); + expect(next.options?.bodyTimeout).toBe(DEFAULT_UNDICI_STREAM_TIMEOUT_MS); + expect(next.options?.headersTimeout).toBe(DEFAULT_UNDICI_STREAM_TIMEOUT_MS); expect(undiciGlobalDispatcherModule._globalUndiciStreamTimeoutMs).toBe( DEFAULT_UNDICI_STREAM_TIMEOUT_MS, ); @@ -226,8 +238,12 @@ describe("ensureGlobalUndiciStreamTimeouts", () => { ensureGlobalUndiciStreamTimeouts({ timeoutMs }); - expect(loadUndiciGlobalDispatcherDeps).not.toHaveBeenCalled(); - expect(setGlobalDispatcher).not.toHaveBeenCalled(); + expect(loadUndiciGlobalDispatcherDeps).toHaveBeenCalledTimes(1); + expect(setGlobalDispatcher).toHaveBeenCalledTimes(1); + const next = getCurrentDispatcher() as { options?: Record }; + expect(next).toBeInstanceOf(Agent); + expect(next.options?.bodyTimeout).toBe(timeoutMs); + expect(next.options?.headersTimeout).toBe(timeoutMs); expect(undiciGlobalDispatcherModule._globalUndiciStreamTimeoutMs).toBe(timeoutMs); }); diff --git a/src/infra/net/undici-global-dispatcher.ts b/src/infra/net/undici-global-dispatcher.ts index 47660ebfde9..f59de617858 100644 --- a/src/infra/net/undici-global-dispatcher.ts +++ b/src/infra/net/undici-global-dispatcher.ts @@ -99,19 +99,12 @@ export function ensureGlobalUndiciStreamTimeouts(opts?: { timeoutMs?: number }): } const timeoutMs = Math.max(DEFAULT_UNDICI_STREAM_TIMEOUT_MS, Math.floor(timeoutMsRaw)); _globalUndiciStreamTimeoutMs = timeoutMs; - if (!hasEnvHttpProxyAgentConfigured()) { - lastAppliedTimeoutKey = null; - return; - } const runtime = loadUndiciGlobalDispatcherDeps(); - const { EnvHttpProxyAgent, setGlobalDispatcher } = runtime; + const { Agent, EnvHttpProxyAgent, setGlobalDispatcher } = runtime; const kind = resolveCurrentDispatcherKind(runtime); if (kind === null) { return; } - if (kind !== "env-proxy") { - return; - } const autoSelectFamily = resolveUndiciAutoSelectFamily(); const nextKey = resolveDispatcherKey({ kind, timeoutMs, autoSelectFamily }); @@ -121,13 +114,23 @@ export function ensureGlobalUndiciStreamTimeouts(opts?: { timeoutMs?: number }): const connect = createUndiciAutoSelectFamilyConnectOptions(autoSelectFamily); try { - const proxyOptions = { - ...resolveEnvHttpProxyAgentOptions(), - bodyTimeout: timeoutMs, - headersTimeout: timeoutMs, - ...(connect ? { connect } : {}), - } as ConstructorParameters[0]; - setGlobalDispatcher(new EnvHttpProxyAgent(proxyOptions)); + if (kind === "env-proxy") { + const proxyOptions = { + ...resolveEnvHttpProxyAgentOptions(), + bodyTimeout: timeoutMs, + headersTimeout: timeoutMs, + ...(connect ? { connect } : {}), + } as ConstructorParameters[0]; + setGlobalDispatcher(new EnvHttpProxyAgent(proxyOptions)); + } else { + setGlobalDispatcher( + new Agent({ + bodyTimeout: timeoutMs, + headersTimeout: timeoutMs, + ...(connect ? { connect } : {}), + }), + ); + } lastAppliedTimeoutKey = nextKey; } catch { // Best-effort hardening only.