fix(browser): default non-finite fetch timeouts

This commit is contained in:
Peter Steinberger
2026-05-28 23:52:40 -04:00
parent b2bdad5bee
commit 3c8ad8cbaa
2 changed files with 25 additions and 2 deletions

View File

@@ -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",

View File

@@ -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<T>(
url: string,
init: RequestInit & { timeoutMs?: number },
): Promise<T> {
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<T>(
url: string,
init?: RequestInit & { timeoutMs?: number },
): Promise<T> {
const timeoutMs = init?.timeoutMs ?? 5000;
const timeoutMs = resolveBrowserFetchTimeoutMs(init?.timeoutMs);
let isDispatcherPath = false;
try {
if (isAbsoluteHttp(url)) {