mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:50:43 +00:00
fix: propagate timeoutMs to guarded dispatchers (local LLM 60s timeout) (#70831)
* fix: propagate timeoutMs to guarded dispatchers Thread timeoutMs through the dispatcher creation chain so that per-request (guarded) dispatchers honor the configured LLM timeout instead of falling back to undici's hardcoded 60s bodyTimeout/headersTimeout. Changes: - undici-runtime.ts: createHttp1Agent/ProxyAgent/EnvHttpProxyAgent now accept timeoutMs and apply bodyTimeout/headersTimeout to dispatcher options - ssrf.ts: createPinnedDispatcher accepts timeoutMs and passes it through - fetch-guard.ts: fetchWithSsrFGuard reads timeout from params or falls back to global dispatcher bodyTimeout via getGlobalDispatcher() - provider-transport-fetch.ts: buildGuardedModelFetch accepts optional timeoutMs and passes it to fetchWithSsrFGuard The global dispatcher timeout (set by ensureGlobalUndiciStreamTimeouts) is still applied to non-guarded requests. Guarded requests (used by LLM transports) now also receive the timeout via a fallback to the global dispatcher when not explicitly provided. Fixes #70829 * fix: resolve fallback timeout via module-level bridge variable Replace dead-code .options.bodyTimeout read in resolveDispatcherTimeoutMs with a module-level bridge (_globalUndiciStreamTimeoutMs) set by ensureGlobalUndiciStreamTimeouts. This avoids reliance on Undici's non-public .options field and ensures guarded dispatchers inherit the configured stream timeout instead of falling back to undici's 60s default. Fixes Greptile P1 and Codex comments on PR #70831 * chore: re-run CI smoke tests * test: cover guarded dispatcher timeout propagation * test: align timeout bridge expectation * docs: note guarded dispatcher timeout fix --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -34,6 +34,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Providers/OpenAI Codex: synthesize the `openai-codex/gpt-5.5` OAuth model row when Codex catalog discovery omits it, so cron and subagent runs do not fail with `Unknown model` while the account is authenticated.
|
||||
- Models/CLI: keep `openclaw models list` read-only while still showing eligible configured-provider rows, so listing models no longer rewrites per-agent `models.json`. (#70847) Thanks @shakkernerd.
|
||||
- Providers/Google: honor the private-network SSRF opt-in for Gemini image generation requests, so trusted proxy setups that resolve Google API hosts to private addresses can use `image_generate`. Fixes #67216.
|
||||
- Agents/transport: propagate configured attempt timeouts into guarded per-request dispatchers, so slow local LLM calls such as Ollama no longer fail at Undici's default 60-second body timeout. Fixes #70829. (#70831) Thanks @DranboFieldston.
|
||||
- Agents/transport: stop embedded runs from lowering the process-wide undici stream timeouts, so slow Gemini image generation and other long-running provider requests no longer inherit short run-attempt headers timeouts. Fixes #70423. Thanks @giangthb.
|
||||
- Providers/OpenRouter: send image-understanding prompts as user text before image parts, restoring non-empty vision responses for OpenRouter multimodal models. Fixes #70410.
|
||||
- Plugins/providers: mirror runtime auth choices in bundled provider manifests and detect `KIMI_API_KEY` for Moonshot/Kimi web search before plugin runtime loads. Thanks @vincentkoc.
|
||||
|
||||
@@ -75,6 +75,25 @@ describe("buildGuardedModelFetch", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("threads explicit transport timeouts into the shared guarded fetch seam", async () => {
|
||||
const { buildGuardedModelFetch } = await import("./provider-transport-fetch.js");
|
||||
const model = {
|
||||
id: "gpt-5.4",
|
||||
provider: "openai",
|
||||
api: "openai-responses",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
} as unknown as Model<"openai-responses">;
|
||||
|
||||
const fetcher = buildGuardedModelFetch(model, 123_456);
|
||||
await fetcher("https://api.openai.com/v1/responses", { method: "POST" });
|
||||
|
||||
expect(fetchWithSsrFGuardMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
timeoutMs: 123_456,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not force explicit debug proxy overrides onto plain HTTP model transports", async () => {
|
||||
process.env.OPENCLAW_DEBUG_PROXY_ENABLED = "1";
|
||||
process.env.OPENCLAW_DEBUG_PROXY_URL = "http://127.0.0.1:7799";
|
||||
|
||||
@@ -150,7 +150,7 @@ function resolveModelRequestPolicy(model: Model<Api>) {
|
||||
});
|
||||
}
|
||||
|
||||
export function buildGuardedModelFetch(model: Model<Api>): typeof fetch {
|
||||
export function buildGuardedModelFetch(model: Model<Api>, timeoutMs?: number): typeof fetch {
|
||||
const requestConfig = resolveModelRequestPolicy(model);
|
||||
const dispatcherPolicy = buildProviderRequestDispatcherPolicy(requestConfig);
|
||||
return async (input, init) => {
|
||||
@@ -185,6 +185,7 @@ export function buildGuardedModelFetch(model: Model<Api>): typeof fetch {
|
||||
},
|
||||
},
|
||||
dispatcherPolicy,
|
||||
timeoutMs,
|
||||
// Provider transport intentionally keeps the secure default and never
|
||||
// replays unsafe request bodies across cross-origin redirects.
|
||||
allowCrossOriginUnsafeRedirectReplay: false,
|
||||
|
||||
@@ -4,6 +4,10 @@ import {
|
||||
GUARDED_FETCH_MODE,
|
||||
retainSafeHeadersForCrossOriginRedirectHeaders,
|
||||
} from "./fetch-guard.js";
|
||||
import {
|
||||
ensureGlobalUndiciStreamTimeouts,
|
||||
resetGlobalUndiciStreamTimeoutsForTests,
|
||||
} from "./undici-global-dispatcher.js";
|
||||
import { TEST_UNDICI_RUNTIME_DEPS_KEY } from "./undici-runtime.js";
|
||||
|
||||
const { agentCtor, envHttpProxyAgentCtor, proxyAgentCtor } = vi.hoisted(() => ({
|
||||
@@ -171,6 +175,7 @@ describe("fetchWithSsrFGuard hardening", () => {
|
||||
envHttpProxyAgentCtor.mockClear();
|
||||
proxyAgentCtor.mockClear();
|
||||
logWarnMock.mockClear();
|
||||
resetGlobalUndiciStreamTimeoutsForTests();
|
||||
Reflect.deleteProperty(globalThis as object, TEST_UNDICI_RUNTIME_DEPS_KEY);
|
||||
});
|
||||
|
||||
@@ -1013,6 +1018,69 @@ describe("fetchWithSsrFGuard hardening", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("applies explicit timeoutMs to guarded direct dispatchers", async () => {
|
||||
(globalThis as Record<string, unknown>)[TEST_UNDICI_RUNTIME_DEPS_KEY] = {
|
||||
Agent: agentCtor,
|
||||
EnvHttpProxyAgent: envHttpProxyAgentCtor,
|
||||
ProxyAgent: proxyAgentCtor,
|
||||
fetch: vi.fn(async () => okResponse()),
|
||||
};
|
||||
const fetchImpl = vi.fn(async () => okResponse());
|
||||
|
||||
const result = await fetchWithSsrFGuard({
|
||||
url: "https://public.example/resource",
|
||||
fetchImpl,
|
||||
lookupFn: createPublicLookup(),
|
||||
timeoutMs: 123_456,
|
||||
});
|
||||
|
||||
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
||||
expect(agentCtor).toHaveBeenCalledWith({
|
||||
connect: expect.objectContaining({
|
||||
lookup: expect.any(Function),
|
||||
}),
|
||||
allowH2: false,
|
||||
bodyTimeout: 123_456,
|
||||
headersTimeout: 123_456,
|
||||
});
|
||||
await result.release();
|
||||
});
|
||||
|
||||
it("inherits the configured global stream timeout for guarded direct dispatchers", async () => {
|
||||
const { getGlobalDispatcher, setGlobalDispatcher } = await import("undici");
|
||||
const previousDispatcher = getGlobalDispatcher();
|
||||
try {
|
||||
ensureGlobalUndiciStreamTimeouts({ timeoutMs: 1_900_000 });
|
||||
(globalThis as Record<string, unknown>)[TEST_UNDICI_RUNTIME_DEPS_KEY] = {
|
||||
Agent: agentCtor,
|
||||
EnvHttpProxyAgent: envHttpProxyAgentCtor,
|
||||
ProxyAgent: proxyAgentCtor,
|
||||
fetch: vi.fn(async () => okResponse()),
|
||||
};
|
||||
const fetchImpl = vi.fn(async () => okResponse());
|
||||
|
||||
const result = await fetchWithSsrFGuard({
|
||||
url: "https://public.example/resource",
|
||||
fetchImpl,
|
||||
lookupFn: createPublicLookup(),
|
||||
});
|
||||
|
||||
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
||||
expect(agentCtor).toHaveBeenCalledWith({
|
||||
connect: expect.objectContaining({
|
||||
lookup: expect.any(Function),
|
||||
}),
|
||||
allowH2: false,
|
||||
bodyTimeout: 1_900_000,
|
||||
headersTimeout: 1_900_000,
|
||||
});
|
||||
await result.release();
|
||||
} finally {
|
||||
setGlobalDispatcher(previousDispatcher);
|
||||
resetGlobalUndiciStreamTimeoutsForTests();
|
||||
}
|
||||
});
|
||||
|
||||
it("allows explicit proxy on localhost when allowPrivateProxy is true even with restrictive hostnameAllowlist", async () => {
|
||||
// Reproduces #61906: Telegram media downloads fail because the SSRF guard
|
||||
// checks the proxy hostname (localhost) against a target-scoped allowlist
|
||||
|
||||
@@ -19,12 +19,25 @@ import {
|
||||
SsrFBlockedError,
|
||||
type SsrFPolicy,
|
||||
} from "./ssrf.js";
|
||||
import { _globalUndiciStreamTimeoutMs } from "./undici-global-dispatcher.js";
|
||||
import {
|
||||
createHttp1Agent,
|
||||
createHttp1EnvHttpProxyAgent,
|
||||
createHttp1ProxyAgent,
|
||||
} from "./undici-runtime.js";
|
||||
|
||||
function resolveDispatcherTimeoutMs(fromParams: number | undefined): number | undefined {
|
||||
if (fromParams !== undefined) {
|
||||
return fromParams;
|
||||
}
|
||||
// Fall back to module-level bridge set by ensureGlobalUndiciStreamTimeouts
|
||||
// (avoids reading Undici's non-public `.options` field)
|
||||
if (_globalUndiciStreamTimeoutMs !== undefined) {
|
||||
return _globalUndiciStreamTimeoutMs;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
type FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
||||
|
||||
export const GUARDED_FETCH_MODE = {
|
||||
@@ -125,6 +138,7 @@ function assertExplicitProxySupportsPinnedDns(
|
||||
|
||||
function createPolicyDispatcherWithoutPinnedDns(
|
||||
dispatcherPolicy?: PinnedDispatcherPolicy,
|
||||
timeoutMs?: number,
|
||||
): Dispatcher | null {
|
||||
if (!dispatcherPolicy) {
|
||||
return null;
|
||||
@@ -133,23 +147,28 @@ function createPolicyDispatcherWithoutPinnedDns(
|
||||
if (dispatcherPolicy.mode === "direct") {
|
||||
return createHttp1Agent(
|
||||
dispatcherPolicy.connect ? { connect: { ...dispatcherPolicy.connect } } : undefined,
|
||||
timeoutMs,
|
||||
);
|
||||
}
|
||||
|
||||
if (dispatcherPolicy.mode === "env-proxy") {
|
||||
return createHttp1EnvHttpProxyAgent({
|
||||
...(dispatcherPolicy.connect ? { connect: { ...dispatcherPolicy.connect } } : {}),
|
||||
...(dispatcherPolicy.proxyTls ? { proxyTls: { ...dispatcherPolicy.proxyTls } } : {}),
|
||||
});
|
||||
return createHttp1EnvHttpProxyAgent(
|
||||
{
|
||||
...(dispatcherPolicy.connect ? { connect: { ...dispatcherPolicy.connect } } : {}),
|
||||
...(dispatcherPolicy.proxyTls ? { proxyTls: { ...dispatcherPolicy.proxyTls } } : {}),
|
||||
},
|
||||
timeoutMs,
|
||||
);
|
||||
}
|
||||
|
||||
const proxyUrl = dispatcherPolicy.proxyUrl.trim();
|
||||
return dispatcherPolicy.proxyTls
|
||||
? createHttp1ProxyAgent({
|
||||
uri: proxyUrl,
|
||||
requestTls: { ...dispatcherPolicy.proxyTls },
|
||||
})
|
||||
: createHttp1ProxyAgent({ uri: proxyUrl });
|
||||
if (dispatcherPolicy.proxyTls) {
|
||||
return createHttp1ProxyAgent(
|
||||
{ uri: proxyUrl, requestTls: { ...dispatcherPolicy.proxyTls } },
|
||||
timeoutMs,
|
||||
);
|
||||
}
|
||||
return createHttp1ProxyAgent({ uri: proxyUrl }, timeoutMs);
|
||||
}
|
||||
|
||||
async function assertExplicitProxyAllowed(
|
||||
@@ -337,25 +356,31 @@ export async function fetchWithSsrFGuard(params: GuardedFetchOptions): Promise<G
|
||||
await assertExplicitProxyAllowed(params.dispatcherPolicy, params.lookupFn, params.policy);
|
||||
const canUseTrustedEnvProxy =
|
||||
mode === GUARDED_FETCH_MODE.TRUSTED_ENV_PROXY && hasProxyEnvConfigured();
|
||||
const timeoutMs = resolveDispatcherTimeoutMs(params.timeoutMs);
|
||||
if (canUseTrustedEnvProxy) {
|
||||
dispatcher = createHttp1EnvHttpProxyAgent();
|
||||
dispatcher = createHttp1EnvHttpProxyAgent(undefined, timeoutMs);
|
||||
} else if (usesTrustedExplicitProxyMode) {
|
||||
// Explicit proxy targets are still checked against the caller's hostname
|
||||
// policy, but the proxy does the DNS resolution for the final target.
|
||||
assertHostnameAllowedWithPolicy(parsedUrl.hostname, params.policy);
|
||||
dispatcher = createPolicyDispatcherWithoutPinnedDns(params.dispatcherPolicy);
|
||||
dispatcher = createPolicyDispatcherWithoutPinnedDns(params.dispatcherPolicy, timeoutMs);
|
||||
} else if (params.pinDns === false) {
|
||||
await resolvePinnedHostnameWithPolicy(parsedUrl.hostname, {
|
||||
lookupFn: params.lookupFn,
|
||||
policy: params.policy,
|
||||
});
|
||||
dispatcher = createPolicyDispatcherWithoutPinnedDns(params.dispatcherPolicy);
|
||||
dispatcher = createPolicyDispatcherWithoutPinnedDns(params.dispatcherPolicy, timeoutMs);
|
||||
} else {
|
||||
const pinned = await resolvePinnedHostnameWithPolicy(parsedUrl.hostname, {
|
||||
lookupFn: params.lookupFn,
|
||||
policy: params.policy,
|
||||
});
|
||||
dispatcher = createPinnedDispatcher(pinned, params.dispatcherPolicy, params.policy);
|
||||
dispatcher = createPinnedDispatcher(
|
||||
pinned,
|
||||
params.dispatcherPolicy,
|
||||
params.policy,
|
||||
timeoutMs,
|
||||
);
|
||||
}
|
||||
|
||||
const init: DispatcherAwareRequestInit = {
|
||||
|
||||
@@ -113,6 +113,26 @@ describe("createPinnedDispatcher", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("applies stream timeouts to pinned direct dispatchers", () => {
|
||||
const lookup = vi.fn() as unknown as PinnedHostname["lookup"];
|
||||
const pinned: PinnedHostname = {
|
||||
hostname: "api.telegram.org",
|
||||
addresses: ["149.154.167.220"],
|
||||
lookup,
|
||||
};
|
||||
|
||||
createPinnedDispatcher(pinned, undefined, undefined, 123_456);
|
||||
|
||||
expect(agentCtor).toHaveBeenCalledWith({
|
||||
connect: {
|
||||
lookup,
|
||||
},
|
||||
allowH2: false,
|
||||
bodyTimeout: 123_456,
|
||||
headersTimeout: 123_456,
|
||||
});
|
||||
});
|
||||
|
||||
it("replaces the pinned lookup when a dispatcher override hostname is provided", () => {
|
||||
const originalLookup = vi.fn() as unknown as PinnedHostname["lookup"];
|
||||
const lookup = createDispatcherWithPinnedOverride(originalLookup);
|
||||
@@ -217,4 +237,37 @@ describe("createPinnedDispatcher", () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("applies stream timeouts to explicit proxy dispatchers", () => {
|
||||
const lookup = vi.fn() as unknown as PinnedHostname["lookup"];
|
||||
const pinned: PinnedHostname = {
|
||||
hostname: "api.telegram.org",
|
||||
addresses: ["149.154.167.220"],
|
||||
lookup,
|
||||
};
|
||||
|
||||
createPinnedDispatcher(
|
||||
pinned,
|
||||
{
|
||||
mode: "explicit-proxy",
|
||||
proxyUrl: "http://127.0.0.1:7890",
|
||||
proxyTls: {
|
||||
autoSelectFamily: false,
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
654_321,
|
||||
);
|
||||
|
||||
expect(proxyAgentCtor).toHaveBeenCalledWith({
|
||||
uri: "http://127.0.0.1:7890",
|
||||
requestTls: {
|
||||
autoSelectFamily: false,
|
||||
lookup,
|
||||
},
|
||||
allowH2: false,
|
||||
bodyTimeout: 654_321,
|
||||
headersTimeout: 654_321,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -448,34 +448,39 @@ export function createPinnedDispatcher(
|
||||
pinned: PinnedHostname,
|
||||
policy?: PinnedDispatcherPolicy,
|
||||
ssrfPolicy?: SsrFPolicy,
|
||||
timeoutMs?: number,
|
||||
): Dispatcher {
|
||||
const lookup = resolvePinnedDispatcherLookup(pinned, policy?.pinnedHostname, ssrfPolicy);
|
||||
|
||||
if (!policy || policy.mode === "direct") {
|
||||
return createHttp1Agent({
|
||||
connect: withPinnedLookup(lookup, policy?.connect),
|
||||
});
|
||||
return createHttp1Agent({ connect: withPinnedLookup(lookup, policy?.connect) }, timeoutMs);
|
||||
}
|
||||
|
||||
if (policy.mode === "env-proxy") {
|
||||
return createHttp1EnvHttpProxyAgent({
|
||||
connect: withPinnedLookup(lookup, policy.connect),
|
||||
...(policy.proxyTls ? { proxyTls: { ...policy.proxyTls } } : {}),
|
||||
});
|
||||
return createHttp1EnvHttpProxyAgent(
|
||||
{
|
||||
connect: withPinnedLookup(lookup, policy.connect),
|
||||
...(policy.proxyTls ? { proxyTls: { ...policy.proxyTls } } : {}),
|
||||
},
|
||||
timeoutMs,
|
||||
);
|
||||
}
|
||||
|
||||
const proxyUrl = policy.proxyUrl.trim();
|
||||
const requestTls = withPinnedLookup(lookup, policy.proxyTls);
|
||||
if (!requestTls) {
|
||||
return createHttp1ProxyAgent({ uri: proxyUrl });
|
||||
return createHttp1ProxyAgent({ uri: proxyUrl }, timeoutMs);
|
||||
}
|
||||
return createHttp1ProxyAgent({
|
||||
uri: proxyUrl,
|
||||
// `PinnedDispatcherPolicy.proxyTls` historically carried target-hop
|
||||
// transport hints for explicit proxies. Translate that to undici's
|
||||
// `requestTls` so HTTPS proxy tunnels keep the pinned DNS lookup.
|
||||
requestTls,
|
||||
});
|
||||
return createHttp1ProxyAgent(
|
||||
{
|
||||
uri: proxyUrl,
|
||||
// `PinnedDispatcherPolicy.proxyTls` historically carried target-hop
|
||||
// transport hints for explicit proxies. Translate that to undici's
|
||||
// `requestTls` so HTTPS proxy tunnels keep the pinned DNS lookup.
|
||||
requestTls,
|
||||
},
|
||||
timeoutMs,
|
||||
);
|
||||
}
|
||||
|
||||
export async function closeDispatcher(dispatcher?: Dispatcher | null): Promise<void> {
|
||||
|
||||
@@ -73,15 +73,17 @@ let DEFAULT_UNDICI_STREAM_TIMEOUT_MS: typeof import("./undici-global-dispatcher.
|
||||
let ensureGlobalUndiciEnvProxyDispatcher: typeof import("./undici-global-dispatcher.js").ensureGlobalUndiciEnvProxyDispatcher;
|
||||
let ensureGlobalUndiciStreamTimeouts: typeof import("./undici-global-dispatcher.js").ensureGlobalUndiciStreamTimeouts;
|
||||
let resetGlobalUndiciStreamTimeoutsForTests: typeof import("./undici-global-dispatcher.js").resetGlobalUndiciStreamTimeoutsForTests;
|
||||
let undiciGlobalDispatcherModule: typeof import("./undici-global-dispatcher.js");
|
||||
|
||||
describe("ensureGlobalUndiciStreamTimeouts", () => {
|
||||
beforeAll(async () => {
|
||||
undiciGlobalDispatcherModule = await import("./undici-global-dispatcher.js");
|
||||
({
|
||||
DEFAULT_UNDICI_STREAM_TIMEOUT_MS,
|
||||
ensureGlobalUndiciEnvProxyDispatcher,
|
||||
ensureGlobalUndiciStreamTimeouts,
|
||||
resetGlobalUndiciStreamTimeoutsForTests,
|
||||
} = await import("./undici-global-dispatcher.js"));
|
||||
} = undiciGlobalDispatcherModule);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -125,12 +127,13 @@ describe("ensureGlobalUndiciStreamTimeouts", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not override unsupported custom proxy dispatcher types", () => {
|
||||
it("records timeout bridge but does not override unsupported custom proxy dispatcher types", () => {
|
||||
setCurrentDispatcher(new ProxyAgent("http://proxy.test:8080"));
|
||||
|
||||
ensureGlobalUndiciStreamTimeouts();
|
||||
ensureGlobalUndiciStreamTimeouts({ timeoutMs: 1_900_000 });
|
||||
|
||||
expect(setGlobalDispatcher).not.toHaveBeenCalled();
|
||||
expect(undiciGlobalDispatcherModule._globalUndiciStreamTimeoutMs).toBe(1_900_000);
|
||||
});
|
||||
|
||||
it("is idempotent for unchanged dispatcher kind and network policy", () => {
|
||||
|
||||
@@ -5,6 +5,13 @@ import { hasEnvHttpProxyConfigured } from "./proxy-env.js";
|
||||
|
||||
export const DEFAULT_UNDICI_STREAM_TIMEOUT_MS = 30 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Module-level bridge so `resolveDispatcherTimeoutMs` in fetch-guard.ts
|
||||
* can read the global dispatcher timeout without relying on Undici's
|
||||
* non-public `.options` field.
|
||||
*/
|
||||
export let _globalUndiciStreamTimeoutMs: number | undefined;
|
||||
|
||||
const AUTO_SELECT_FAMILY_ATTEMPT_TIMEOUT_MS = 300;
|
||||
|
||||
let lastAppliedTimeoutKey: string | null = null;
|
||||
@@ -114,6 +121,7 @@ export function ensureGlobalUndiciStreamTimeouts(opts?: { timeoutMs?: number }):
|
||||
return;
|
||||
}
|
||||
const timeoutMs = Math.max(DEFAULT_UNDICI_STREAM_TIMEOUT_MS, Math.floor(timeoutMsRaw));
|
||||
_globalUndiciStreamTimeoutMs = timeoutMs;
|
||||
const kind = resolveCurrentDispatcherKind();
|
||||
if (kind === null) {
|
||||
return;
|
||||
@@ -152,4 +160,5 @@ export function ensureGlobalUndiciStreamTimeouts(opts?: { timeoutMs?: number }):
|
||||
export function resetGlobalUndiciStreamTimeoutsForTests(): void {
|
||||
lastAppliedTimeoutKey = null;
|
||||
lastAppliedProxyBootstrap = false;
|
||||
_globalUndiciStreamTimeoutMs = undefined;
|
||||
}
|
||||
|
||||
@@ -53,36 +53,47 @@ export function loadUndiciRuntimeDeps(): UndiciRuntimeDeps {
|
||||
|
||||
function withHttp1OnlyDispatcherOptions<T extends object | undefined>(
|
||||
options?: T,
|
||||
timeoutMs?: number,
|
||||
): (T extends object ? T : Record<never, never>) & { allowH2: false } {
|
||||
if (!options) {
|
||||
return { ...HTTP1_ONLY_DISPATCHER_OPTIONS } as (T extends object ? T : Record<never, never>) & {
|
||||
allowH2: false;
|
||||
};
|
||||
const base = {} as (T extends object ? T : Record<never, never>) & { allowH2: false };
|
||||
if (options) {
|
||||
Object.assign(base, options);
|
||||
}
|
||||
return {
|
||||
...options,
|
||||
...HTTP1_ONLY_DISPATCHER_OPTIONS,
|
||||
} as (T extends object ? T : Record<never, never>) & { allowH2: false };
|
||||
// Enforce HTTP/1.1-only — must come after options to prevent accidental override
|
||||
Object.assign(base, HTTP1_ONLY_DISPATCHER_OPTIONS);
|
||||
if (timeoutMs !== undefined && Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
||||
(base as Record<string, unknown>).bodyTimeout = timeoutMs;
|
||||
(base as Record<string, unknown>).headersTimeout = timeoutMs;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
export function createHttp1Agent(options?: UndiciAgentOptions): import("undici").Agent {
|
||||
export function createHttp1Agent(
|
||||
options?: UndiciAgentOptions,
|
||||
timeoutMs?: number,
|
||||
): import("undici").Agent {
|
||||
const { Agent } = loadUndiciRuntimeDeps();
|
||||
return new Agent(withHttp1OnlyDispatcherOptions(options));
|
||||
return new Agent(withHttp1OnlyDispatcherOptions(options, timeoutMs));
|
||||
}
|
||||
|
||||
export function createHttp1EnvHttpProxyAgent(
|
||||
options?: UndiciEnvHttpProxyAgentOptions,
|
||||
timeoutMs?: number,
|
||||
): import("undici").EnvHttpProxyAgent {
|
||||
const { EnvHttpProxyAgent } = loadUndiciRuntimeDeps();
|
||||
return new EnvHttpProxyAgent(withHttp1OnlyDispatcherOptions(options));
|
||||
return new EnvHttpProxyAgent(withHttp1OnlyDispatcherOptions(options, timeoutMs));
|
||||
}
|
||||
|
||||
export function createHttp1ProxyAgent(
|
||||
options: UndiciProxyAgentOptions,
|
||||
timeoutMs?: number,
|
||||
): import("undici").ProxyAgent {
|
||||
const { ProxyAgent } = loadUndiciRuntimeDeps();
|
||||
if (typeof options === "string" || options instanceof URL) {
|
||||
return new ProxyAgent(withHttp1OnlyDispatcherOptions({ uri: options.toString() }));
|
||||
}
|
||||
return new ProxyAgent(withHttp1OnlyDispatcherOptions(options));
|
||||
const normalized =
|
||||
typeof options === "string" || options instanceof URL
|
||||
? { uri: options.toString() }
|
||||
: { ...options };
|
||||
return new ProxyAgent(
|
||||
withHttp1OnlyDispatcherOptions(normalized as object, timeoutMs) as UndiciProxyAgentOptions,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user