diff --git a/extensions/browser/src/browser/client-fetch.loopback-auth.test.ts b/extensions/browser/src/browser/client-fetch.loopback-auth.test.ts index e207e60924ce..6d7bb87b2002 100644 --- a/extensions/browser/src/browser/client-fetch.loopback-auth.test.ts +++ b/extensions/browser/src/browser/client-fetch.loopback-auth.test.ts @@ -543,6 +543,23 @@ describe("fetchBrowserJson loopback auth", () => { ); }); + it("uses the default timeout for non-finite absolute HTTP timeout failures", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => { + throw new Error("timed out"); + }), + ); + + await expectThrownBrowserFetchError( + () => fetchBrowserJson<{ ok: boolean }>("http://example.com/", { timeoutMs: Number.NaN }), + { + contains: ["timed out after 5000ms"], + omits: ["NaNms", "Do NOT retry the browser tool"], + }, + ); + }); + it("omits no-retry hint for absolute HTTP abort failures", async () => { vi.stubGlobal( "fetch", diff --git a/extensions/browser/src/browser/client-fetch.ts b/extensions/browser/src/browser/client-fetch.ts index d65a14e927c2..a9bed1c0d03f 100644 --- a/extensions/browser/src/browser/client-fetch.ts +++ b/extensions/browser/src/browser/client-fetch.ts @@ -1,3 +1,4 @@ +import { parseFiniteNumber } from "openclaw/plugin-sdk/number-runtime"; import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime"; import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime"; import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime"; @@ -162,6 +163,11 @@ function appendBrowserToolModelHint(message: string): string { type BrowserFetchFailureKind = "timeout" | "aborted" | "persistent"; +function resolveBrowserFetchTimeoutMs(timeoutMs: number | undefined): number { + const parsed = parseFiniteNumber(timeoutMs); + return Math.max(1, Math.floor(parsed ?? 5000)); +} + function classifyBrowserFetchFailure(err: unknown): BrowserFetchFailureKind { const msg = normalizeErrorMessage(err); const msgLower = normalizeLowercaseStringOrEmpty(msg); @@ -228,7 +234,7 @@ async function fetchHttpJson( url: string, init: RequestInit & { timeoutMs?: number }, ): Promise { - const timeoutMs = init.timeoutMs ?? 5000; + const timeoutMs = resolveBrowserFetchTimeoutMs(init.timeoutMs); const ctrl = new AbortController(); const upstreamSignal = init.signal; let upstreamAbortListener: (() => void) | undefined; @@ -278,7 +284,7 @@ export async function fetchBrowserJson( url: string, init?: RequestInit & { timeoutMs?: number }, ): Promise { - const timeoutMs = init?.timeoutMs ?? 5000; + const timeoutMs = resolveBrowserFetchTimeoutMs(init?.timeoutMs); let isDispatcherPath = false; try { if (isAbsoluteHttp(url)) {