mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 18:30:44 +00:00
fix: lazy-load undici dispatchers
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user