fix: lazy-load undici dispatchers

This commit is contained in:
Shakker
2026-05-06 01:02:46 +01:00
committed by Shakker
parent 98cbf7f11c
commit 85ed972217
3 changed files with 108 additions and 43 deletions

View File

@@ -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<UndiciRuntimeDeps["FormData"]>;
type UndiciFormDataInstance = InstanceType<UndiciFormDataCtor>;
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<UndiciRuntimeDeps["ProxyAgent"]> | null = null;
const resolveAgent = (): InstanceType<UndiciRuntimeDeps["ProxyAgent"]> => {
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<string, unknown>),
...(normalizeInitForUndici(init, UndiciFormData) as Record<string, unknown>),
dispatcher: resolveAgent(),
}) as unknown as Promise<Response>) 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<string, unknown>),
...(normalizeInitForUndici(init, UndiciFormData) as Record<string, unknown>),
dispatcher: agent,
}) as unknown as Promise<Response>) as typeof fetch;
} catch (err) {

View File

@@ -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<UndiciGlobalDispatcherDeps, "getGlobalDispatcher">,
): 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<typeof EnvHttpProxyAgent>[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<UndiciGlobalDispatcherDeps["EnvHttpProxyAgent"]>[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<typeof EnvHttpProxyAgent>[0]),
);
lastAppliedProxyBootstrap = true;
} else {
setGlobalDispatcher(new Agent());
}
setGlobalDispatcher(
new EnvHttpProxyAgent(
proxyOptions as ConstructorParameters<UndiciGlobalDispatcherDeps["EnvHttpProxyAgent"]>[0],
),
);
lastAppliedProxyBootstrap = true;
} catch {
// Best-effort reset only.
}

View File

@@ -11,6 +11,11 @@ export type UndiciRuntimeDeps = {
fetch: typeof import("undici").fetch;
};
export type UndiciGlobalDispatcherDeps = Pick<UndiciRuntimeDeps, "Agent" | "EnvHttpProxyAgent"> & {
getGlobalDispatcher: typeof import("undici").getGlobalDispatcher;
setGlobalDispatcher: typeof import("undici").setGlobalDispatcher;
};
type UndiciAgentOptions = ConstructorParameters<UndiciRuntimeDeps["Agent"]>[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<string, unknown>)[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<string, unknown>)[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<T extends object | undefined>(
options?: T,
timeoutMs?: number,