diff --git a/extensions/browser/src/gateway/browser-request.timeout.test.ts b/extensions/browser/src/gateway/browser-request.timeout.test.ts index 0714cf0d237..9feedf88d1e 100644 --- a/extensions/browser/src/gateway/browser-request.timeout.test.ts +++ b/extensions/browser/src/gateway/browser-request.timeout.test.ts @@ -1,3 +1,4 @@ +import { MAX_TIMER_TIMEOUT_MS } from "openclaw/plugin-sdk/number-runtime"; import { beforeEach, describe, expect, it, vi } from "vitest"; const { @@ -75,4 +76,27 @@ describe("browser.request local timeout", () => { message: "Error: browser request timed out", }); }); + + it("caps timeoutMs before local browser dispatches", async () => { + const respond = vi.fn(); + + await browserHandlers["browser.request"]({ + params: { + method: "POST", + path: "/tabs/open", + body: { url: "https://example.com" }, + timeoutMs: Number.MAX_SAFE_INTEGER, + }, + respond: respond as never, + context: { + nodeRegistry: { listConnected: () => [] }, + } as never, + client: null, + req: { type: "req", id: "req-1", method: "browser.request" }, + isWebchatConnect: () => false, + }); + + const [, timeoutMs] = withTimeoutMock.mock.calls.at(-1) ?? []; + expect(timeoutMs).toBe(MAX_TIMER_TIMEOUT_MS); + }); }); diff --git a/extensions/browser/src/gateway/browser-request.ts b/extensions/browser/src/gateway/browser-request.ts index d8c22a6fcc3..b634ffe21f4 100644 --- a/extensions/browser/src/gateway/browser-request.ts +++ b/extensions/browser/src/gateway/browser-request.ts @@ -1,4 +1,5 @@ import crypto from "node:crypto"; +import { clampTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime"; import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, @@ -139,10 +140,7 @@ export async function handleBrowserGatewayRequest({ const path = normalizeOptionalString(typed.path) ?? ""; const query = typed.query && typeof typed.query === "object" ? typed.query : undefined; const body = typed.body; - const timeoutMs = - typeof typed.timeoutMs === "number" && Number.isFinite(typed.timeoutMs) - ? Math.max(1, Math.floor(typed.timeoutMs)) - : undefined; + const timeoutMs = clampTimerTimeoutMs(typed.timeoutMs); if (!methodRaw || !path) { respond( diff --git a/extensions/browser/src/node-host/invoke-browser.test.ts b/extensions/browser/src/node-host/invoke-browser.test.ts index cffcc0b073a..90c274e22af 100644 --- a/extensions/browser/src/node-host/invoke-browser.test.ts +++ b/extensions/browser/src/node-host/invoke-browser.test.ts @@ -1,3 +1,4 @@ +import { MAX_TIMER_TIMEOUT_MS } from "openclaw/plugin-sdk/number-runtime"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const controlServiceMocks = vi.hoisted(() => ({ @@ -452,6 +453,29 @@ describe("runBrowserProxyCommand", () => { expect(request.query).toEqual({ profile: "openclaw" }); }); + it("caps browser proxy command timeout before dispatch", async () => { + dispatcherMocks.dispatch.mockResolvedValue({ + status: 200, + body: { ok: true }, + }); + const timeoutSpy = vi + .spyOn(globalThis, "setTimeout") + .mockReturnValue(1 as unknown as ReturnType); + + try { + await runBrowserProxyCommand( + JSON.stringify({ + method: "GET", + path: "/snapshot", + timeoutMs: Number.MAX_SAFE_INTEGER, + }), + ); + expect(timeoutSpy).toHaveBeenCalledWith(expect.any(Function), MAX_TIMER_TIMEOUT_MS); + } finally { + timeoutSpy.mockRestore(); + } + }); + it("rejects persistent profile creation when allowProfiles is empty", async () => { await expect( runBrowserProxyCommand( diff --git a/extensions/browser/src/node-host/invoke-browser.ts b/extensions/browser/src/node-host/invoke-browser.ts index e2a95678176..d4d1c0016ec 100644 --- a/extensions/browser/src/node-host/invoke-browser.ts +++ b/extensions/browser/src/node-host/invoke-browser.ts @@ -1,4 +1,5 @@ import fsPromises from "node:fs/promises"; +import { resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime"; import { normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime"; import { redactCdpUrl } from "../browser/cdp.helpers.js"; import { loadBrowserConfigForRuntimeRefresh } from "../browser/config-refresh-source.js"; @@ -136,9 +137,7 @@ function decodeParams(raw?: string | null): T { } function resolveBrowserProxyTimeout(timeoutMs?: number): number { - return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) - ? Math.max(1, Math.floor(timeoutMs)) - : DEFAULT_BROWSER_PROXY_TIMEOUT_MS; + return resolveTimerTimeoutMs(timeoutMs, DEFAULT_BROWSER_PROXY_TIMEOUT_MS); } function isBrowserProxyTimeoutError(err: unknown): boolean {