From 85ed972217b7c857522a3a9b6249b748d301756f Mon Sep 17 00:00:00 2001 From: Shakker Date: Wed, 6 May 2026 01:02:46 +0100 Subject: [PATCH] fix: lazy-load undici dispatchers --- src/infra/net/proxy-fetch.ts | 39 +++++++---- src/infra/net/undici-global-dispatcher.ts | 80 ++++++++++++++--------- src/infra/net/undici-runtime.ts | 32 +++++++++ 3 files changed, 108 insertions(+), 43 deletions(-) diff --git a/src/infra/net/proxy-fetch.ts b/src/infra/net/proxy-fetch.ts index dbe12131f82..5200e3a392f 100644 --- a/src/infra/net/proxy-fetch.ts +++ b/src/infra/net/proxy-fetch.ts @@ -1,12 +1,7 @@ -import { - EnvHttpProxyAgent, - FormData as UndiciFormData, - ProxyAgent, - fetch as undiciFetch, -} from "undici"; import { logWarn } from "../../logger.js"; import { formatErrorMessage } from "../errors.js"; import { resolveEnvHttpProxyAgentOptions } from "./proxy-env.js"; +import { loadUndiciRuntimeDeps, type UndiciRuntimeDeps } from "./undici-runtime.js"; export const PROXY_FETCH_PROXY_URL = Symbol.for("openclaw.proxyFetch.proxyUrl"); type ProxyFetchWithMetadata = typeof fetch & { @@ -22,7 +17,14 @@ function isFormDataLike(value: unknown): value is FormData { ); } -function appendFormDataEntry(target: UndiciFormData, key: string, value: FormDataEntryValue): void { +type UndiciFormDataCtor = NonNullable; +type UndiciFormDataInstance = InstanceType; + +function appendFormDataEntry( + target: UndiciFormDataInstance, + key: string, + value: FormDataEntryValue, +): void { if (typeof value === "string") { target.append(key, value); return; @@ -35,7 +37,10 @@ function appendFormDataEntry(target: UndiciFormData, key: string, value: FormDat target.append(key, value); } -function normalizeInitForUndici(init: RequestInit | undefined): RequestInit | undefined { +function normalizeInitForUndici( + init: RequestInit | undefined, + UndiciFormData: UndiciFormDataCtor, +): RequestInit | undefined { if (!init) { return init; } @@ -58,8 +63,13 @@ function normalizeInitForUndici(init: RequestInit | undefined): RequestInit | un * Uses undici's ProxyAgent under the hood. */ export function makeProxyFetch(proxyUrl: string): typeof fetch { - let agent: ProxyAgent | null = null; - const resolveAgent = (): ProxyAgent => { + const { + ProxyAgent, + FormData: UndiciFormData = globalThis.FormData as unknown as UndiciFormDataCtor, + fetch: undiciFetch, + } = loadUndiciRuntimeDeps(); + let agent: InstanceType | null = null; + const resolveAgent = (): InstanceType => { if (!agent) { agent = new ProxyAgent(proxyUrl); } @@ -69,7 +79,7 @@ export function makeProxyFetch(proxyUrl: string): typeof fetch { // on stream/body internals. Single cast at the boundary keeps the rest type-safe. const proxyFetch = ((input: RequestInfo | URL, init?: RequestInit) => undiciFetch(input as string | URL, { - ...(normalizeInitForUndici(init) as Record), + ...(normalizeInitForUndici(init, UndiciFormData) as Record), dispatcher: resolveAgent(), }) as unknown as Promise) as ProxyFetchWithMetadata; Object.defineProperty(proxyFetch, PROXY_FETCH_PROXY_URL, { @@ -104,10 +114,15 @@ export function resolveProxyFetchFromEnv( return undefined; } try { + const { + EnvHttpProxyAgent, + FormData: UndiciFormData = globalThis.FormData as unknown as UndiciFormDataCtor, + fetch: undiciFetch, + } = loadUndiciRuntimeDeps(); const agent = new EnvHttpProxyAgent(proxyOptions); return ((input: RequestInfo | URL, init?: RequestInit) => undiciFetch(input as string | URL, { - ...(normalizeInitForUndici(init) as Record), + ...(normalizeInitForUndici(init, UndiciFormData) as Record), dispatcher: agent, }) as unknown as Promise) as typeof fetch; } catch (err) { diff --git a/src/infra/net/undici-global-dispatcher.ts b/src/infra/net/undici-global-dispatcher.ts index 9ee4fbb52f2..47660ebfde9 100644 --- a/src/infra/net/undici-global-dispatcher.ts +++ b/src/infra/net/undici-global-dispatcher.ts @@ -1,9 +1,12 @@ -import { Agent, EnvHttpProxyAgent, getGlobalDispatcher, setGlobalDispatcher } from "undici"; import { hasEnvHttpProxyAgentConfigured, resolveEnvHttpProxyAgentOptions } from "./proxy-env.js"; import { createUndiciAutoSelectFamilyConnectOptions, resolveUndiciAutoSelectFamily, } from "./undici-family-policy.js"; +import { + loadUndiciGlobalDispatcherDeps, + type UndiciGlobalDispatcherDeps, +} from "./undici-runtime.js"; export const DEFAULT_UNDICI_STREAM_TIMEOUT_MS = 30 * 60 * 1000; @@ -46,10 +49,12 @@ function resolveDispatcherKey(params: { return `${params.kind}:${params.timeoutMs}:${autoSelectToken}`; } -function resolveCurrentDispatcherKind(): DispatcherKind | null { +function resolveCurrentDispatcherKind( + runtime: Pick, +): DispatcherKind | null { let dispatcher: unknown; try { - dispatcher = getGlobalDispatcher(); + dispatcher = runtime.getGlobalDispatcher(); } catch { return null; } @@ -63,13 +68,15 @@ export function ensureGlobalUndiciEnvProxyDispatcher(): void { if (!shouldUseEnvProxy) { return; } + const runtime = loadUndiciGlobalDispatcherDeps(); + const { EnvHttpProxyAgent, setGlobalDispatcher } = runtime; if (lastAppliedProxyBootstrap) { - if (resolveCurrentDispatcherKind() === "env-proxy") { + if (resolveCurrentDispatcherKind(runtime) === "env-proxy") { return; } lastAppliedProxyBootstrap = false; } - const currentKind = resolveCurrentDispatcherKind(); + const currentKind = resolveCurrentDispatcherKind(runtime); if (currentKind === null) { return; } @@ -92,10 +99,19 @@ export function ensureGlobalUndiciStreamTimeouts(opts?: { timeoutMs?: number }): } const timeoutMs = Math.max(DEFAULT_UNDICI_STREAM_TIMEOUT_MS, Math.floor(timeoutMsRaw)); _globalUndiciStreamTimeoutMs = timeoutMs; - const kind = resolveCurrentDispatcherKind(); + if (!hasEnvHttpProxyAgentConfigured()) { + lastAppliedTimeoutKey = null; + return; + } + const runtime = loadUndiciGlobalDispatcherDeps(); + const { 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 }); @@ -105,23 +121,13 @@ export function ensureGlobalUndiciStreamTimeouts(opts?: { timeoutMs?: number }): const connect = createUndiciAutoSelectFamilyConnectOptions(autoSelectFamily); try { - 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 } : {}), - }), - ); - } + const proxyOptions = { + ...resolveEnvHttpProxyAgentOptions(), + bodyTimeout: timeoutMs, + headersTimeout: timeoutMs, + ...(connect ? { connect } : {}), + } as ConstructorParameters[0]; + setGlobalDispatcher(new EnvHttpProxyAgent(proxyOptions)); lastAppliedTimeoutKey = nextKey; } catch { // Best-effort hardening only. @@ -140,17 +146,29 @@ export function resetGlobalUndiciStreamTimeoutsForTests(): void { */ export function forceResetGlobalDispatcher(): void { lastAppliedTimeoutKey = null; + if (!hasEnvHttpProxyAgentConfigured()) { + if (!lastAppliedProxyBootstrap) { + return; + } + lastAppliedProxyBootstrap = false; + try { + const { Agent, setGlobalDispatcher } = loadUndiciGlobalDispatcherDeps(); + setGlobalDispatcher(new Agent()); + } catch { + // Best-effort reset only. + } + return; + } lastAppliedProxyBootstrap = false; try { + const { EnvHttpProxyAgent, setGlobalDispatcher } = loadUndiciGlobalDispatcherDeps(); const proxyOptions = resolveEnvHttpProxyAgentOptions(); - if (hasEnvHttpProxyAgentConfigured()) { - setGlobalDispatcher( - new EnvHttpProxyAgent(proxyOptions as ConstructorParameters[0]), - ); - lastAppliedProxyBootstrap = true; - } else { - setGlobalDispatcher(new Agent()); - } + setGlobalDispatcher( + new EnvHttpProxyAgent( + proxyOptions as ConstructorParameters[0], + ), + ); + lastAppliedProxyBootstrap = true; } catch { // Best-effort reset only. } diff --git a/src/infra/net/undici-runtime.ts b/src/infra/net/undici-runtime.ts index 98a64e62c84..7bd5eef5608 100644 --- a/src/infra/net/undici-runtime.ts +++ b/src/infra/net/undici-runtime.ts @@ -11,6 +11,11 @@ export type UndiciRuntimeDeps = { fetch: typeof import("undici").fetch; }; +export type UndiciGlobalDispatcherDeps = Pick & { + getGlobalDispatcher: typeof import("undici").getGlobalDispatcher; + setGlobalDispatcher: typeof import("undici").setGlobalDispatcher; +}; + type UndiciAgentOptions = ConstructorParameters[0]; type UndiciEnvHttpProxyAgentOptions = ConstructorParameters< UndiciRuntimeDeps["EnvHttpProxyAgent"] @@ -50,6 +55,17 @@ function isUndiciRuntimeDeps(value: unknown): value is UndiciRuntimeDeps { ); } +function isUndiciGlobalDispatcherDeps(value: unknown): value is UndiciGlobalDispatcherDeps { + return ( + typeof value === "object" && + value !== null && + typeof (value as UndiciGlobalDispatcherDeps).Agent === "function" && + typeof (value as UndiciGlobalDispatcherDeps).EnvHttpProxyAgent === "function" && + typeof (value as UndiciGlobalDispatcherDeps).getGlobalDispatcher === "function" && + typeof (value as UndiciGlobalDispatcherDeps).setGlobalDispatcher === "function" + ); +} + export function loadUndiciRuntimeDeps(): UndiciRuntimeDeps { const override = (globalThis as Record)[TEST_UNDICI_RUNTIME_DEPS_KEY]; if (isUndiciRuntimeDeps(override)) { @@ -67,6 +83,22 @@ export function loadUndiciRuntimeDeps(): UndiciRuntimeDeps { }; } +export function loadUndiciGlobalDispatcherDeps(): UndiciGlobalDispatcherDeps { + const override = (globalThis as Record)[TEST_UNDICI_RUNTIME_DEPS_KEY]; + if (isUndiciGlobalDispatcherDeps(override)) { + return override; + } + + const require = createRequire(import.meta.url); + const undici = require("undici") as typeof import("undici"); + return { + Agent: undici.Agent, + EnvHttpProxyAgent: undici.EnvHttpProxyAgent, + getGlobalDispatcher: undici.getGlobalDispatcher, + setGlobalDispatcher: undici.setGlobalDispatcher, + }; +} + function withHttp1OnlyDispatcherOptions( options?: T, timeoutMs?: number,