From 425a7a99c49b743778380988bc078e710cc6f6fb Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 26 Feb 2026 20:55:23 +0100 Subject: [PATCH] fix: add browser error passthrough regression coverage (#26380) (thanks @TarasShyn) --- CHANGELOG.md | 1 + .../client-fetch.loopback-auth.test.ts | 42 ++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a677f771ff9..dd314b38eb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,7 @@ Docs: https://docs.openclaw.ai - Browser/Extension relay shutdown: flush pending extension-request timers/rejections during relay `stop()` before socket/server teardown so in-flight extension waits do not survive shutdown windows. Landed from contributor PR #24142 by @kevinWangSheng. - Browser/Extension relay reconnect resilience: keep CDP clients alive across brief MV3 extension disconnect windows, wait briefly for extension reconnect before failing in-flight CDP commands, and only tear down relay target/client state after reconnect grace expires. Landed from contributor PR #27617 by @davidemanuelDEV. - Browser/Route decode hardening: guard malformed percent-encoding in relay target action routes and browser route-param decoding so crafted `%` paths return `400` instead of crashing/unhandled URI decode failures. Landed from contributor PR #11880 by @Yida-Dev. +- Browser/Error visibility: preserve browser-control application error messages (HTTP 4xx/5xx) instead of rewriting them as generic reachability failures. Landed from contributor PR #26380 by @TarasShyn. - Feishu/Permission error dispatch: merge sender-name permission notices into the main inbound dispatch so one user message produces one agent turn/reply (instead of a duplicate permission-notice turn), with regression coverage. (#27381) thanks @byungsker. - Feishu/Inbound message metadata: include inbound `message_id` in `BodyForAgent` on a dedicated metadata line so agents can reliably correlate and act on media/message operations that require message IDs, with regression coverage. (#27253) thanks @xss925175263. - Feishu/Doc tools: route `feishu_doc` and `feishu_app_scopes` through the active agent account context (with explicit `accountId` override support) so multi-account agents no longer default to the first configured app, with regression coverage for context routing and explicit override behavior. (#27338) thanks @AaronL725. diff --git a/src/browser/client-fetch.loopback-auth.test.ts b/src/browser/client-fetch.loopback-auth.test.ts index 3dc17e72730..e71bf08fde1 100644 --- a/src/browser/client-fetch.loopback-auth.test.ts +++ b/src/browser/client-fetch.loopback-auth.test.ts @@ -8,6 +8,10 @@ const mocks = vi.hoisted(() => ({ }, }, })), + dispatch: vi.fn<() => Promise<{ status: number; body: unknown }>>(async () => ({ + status: 200, + body: { ok: true }, + })), })); vi.mock("../config/config.js", async (importOriginal) => { @@ -25,7 +29,7 @@ vi.mock("./control-service.js", () => ({ vi.mock("./routes/dispatcher.js", () => ({ createBrowserRouteDispatcher: vi.fn(() => ({ - dispatch: vi.fn(async () => ({ status: 200, body: { ok: true } })), + dispatch: mocks.dispatch, })), })); @@ -47,6 +51,8 @@ describe("fetchBrowserJson loopback auth", () => { beforeEach(() => { vi.restoreAllMocks(); mocks.loadConfig.mockClear(); + mocks.dispatch.mockClear(); + mocks.dispatch.mockResolvedValue({ status: 200, body: { ok: true } }); mocks.loadConfig.mockReturnValue({ gateway: { auth: { @@ -60,6 +66,15 @@ describe("fetchBrowserJson loopback auth", () => { vi.unstubAllGlobals(); }); + async function expectServiceError(promise: Promise, expectedMessage: string) { + await expect(promise).rejects.toThrow(expectedMessage); + try { + await promise; + } catch (error) { + expect(String(error)).not.toContain("Can't reach the OpenClaw browser control service"); + } + } + it("adds bearer auth for loopback absolute HTTP URLs", async () => { const fetchMock = stubJsonFetchOk(); @@ -114,4 +129,29 @@ describe("fetchBrowserJson loopback auth", () => { const headers = new Headers(init?.headers); expect(headers.get("authorization")).toBe("Bearer loopback-token"); }); + + it("keeps absolute HTTP service errors unwrapped", async () => { + const fetchMock = vi.fn<(input: RequestInfo | URL, init?: RequestInit) => Promise>( + async () => + new Response("browser route failed", { + status: 502, + headers: { "Content-Type": "text/plain" }, + }), + ); + vi.stubGlobal("fetch", fetchMock); + + await expectServiceError( + fetchBrowserJson<{ ok: boolean }>("http://example.com/"), + "browser route failed", + ); + }); + + it("keeps local dispatcher service errors unwrapped", async () => { + mocks.dispatch.mockResolvedValueOnce({ + status: 500, + body: { error: "target unavailable" }, + }); + + await expectServiceError(fetchBrowserJson<{ ok: boolean }>("/json/list"), "target unavailable"); + }); });