From eb273a8a4a07cb93a4a4cabe51532ce98ee3ec3d Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Thu, 28 May 2026 13:28:26 +0200 Subject: [PATCH] fix(brave): bound search error bodies --- .../src/brave-web-search-provider.runtime.ts | 19 ++--- .../src/brave-web-search-provider.test.ts | 77 +++++++++++++++++++ 2 files changed, 83 insertions(+), 13 deletions(-) diff --git a/extensions/brave/src/brave-web-search-provider.runtime.ts b/extensions/brave/src/brave-web-search-provider.runtime.ts index f947f72f84c..ff5093e6dc3 100644 --- a/extensions/brave/src/brave-web-search-provider.runtime.ts +++ b/extensions/brave/src/brave-web-search-provider.runtime.ts @@ -1,4 +1,7 @@ -import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http"; +import { + assertOkOrThrowProviderError, + readProviderJsonResponse, +} from "openclaw/plugin-sdk/provider-http"; import type { SearchConfigRecord } from "openclaw/plugin-sdk/provider-web-search"; import { buildSearchCacheKey, @@ -225,12 +228,7 @@ async function runBraveLlmContextSearch(params: { ok: response.ok, durationMs: Date.now() - startedAt, }); - if (!response.ok) { - const detail = await response.text(); - throw new Error( - `Brave LLM Context API error (${response.status}): ${detail || response.statusText}`, - ); - } + await assertOkOrThrowProviderError(response, "Brave LLM Context API error"); const data = await readProviderJsonResponse( response, @@ -312,12 +310,7 @@ async function runBraveWebSearch(params: { ok: response.ok, durationMs: Date.now() - startedAt, }); - if (!response.ok) { - const detail = await response.text(); - throw new Error( - `Brave Search API error (${response.status}): ${detail || response.statusText}`, - ); - } + await assertOkOrThrowProviderError(response, "Brave Search API error"); const data = await readProviderJsonResponse( response, diff --git a/extensions/brave/src/brave-web-search-provider.test.ts b/extensions/brave/src/brave-web-search-provider.test.ts index 5fcb25a9860..af352949db7 100644 --- a/extensions/brave/src/brave-web-search-provider.test.ts +++ b/extensions/brave/src/brave-web-search-provider.test.ts @@ -88,6 +88,23 @@ function fetchRequestInit(mockFetch: { mock: { calls: Array> } }, return fetchCall(mockFetch, index)[1]; } +function createBodyOnlyErrorResponse(params: { body: string; status: number }): Response { + const bytes = new TextEncoder().encode(params.body); + const body = new ReadableStream({ + start(controller) { + controller.enqueue(bytes); + controller.close(); + }, + }); + return { + ok: false, + status: params.status, + statusText: "Too Many Requests", + headers: new Headers(), + body, + } as Response; +} + describe("brave web search provider", () => { const priorFetch = global.fetch; @@ -347,6 +364,66 @@ describe("brave web search provider", () => { ); }); + it("bounds Brave web error bodies without using response.text", async () => { + vi.stubEnv("BRAVE_API_KEY", ""); + const mockFetch = vi.fn(async (_input?: unknown, _init?: unknown) => + createBodyOnlyErrorResponse({ + status: 429, + body: `${"x".repeat(24 * 1024)}tail-marker`, + }), + ); + global.fetch = mockFetch as typeof global.fetch; + + const provider = createBraveWebSearchProvider(); + const tool = provider.createTool({ + config: {}, + searchConfig: { + apiKey: "brave-test-key", + brave: { mode: "web" }, + }, + }); + if (!tool) { + throw new Error("Expected tool definition"); + } + + const error = await tool.execute({ query: "latest ai news" }).catch((value: unknown) => value); + expect(error).toBeInstanceOf(Error); + const message = error instanceof Error ? error.message : String(error); + expect(message).toContain("Brave Search API error (429):"); + expect(message).not.toContain("tail-marker"); + expect(message.length).toBeLessThan(700); + }); + + it("bounds Brave llm-context error bodies without using response.text", async () => { + vi.stubEnv("BRAVE_API_KEY", ""); + const mockFetch = vi.fn(async (_input?: unknown, _init?: unknown) => + createBodyOnlyErrorResponse({ + status: 429, + body: `${"x".repeat(24 * 1024)}tail-marker`, + }), + ); + global.fetch = mockFetch as typeof global.fetch; + + const provider = createBraveWebSearchProvider(); + const tool = provider.createTool({ + config: {}, + searchConfig: { + apiKey: "brave-test-key", + brave: { mode: "llm-context" }, + }, + }); + if (!tool) { + throw new Error("Expected tool definition"); + } + + const error = await tool.execute({ query: "latest ai news" }).catch((value: unknown) => value); + expect(error).toBeInstanceOf(Error); + const message = error instanceof Error ? error.message : String(error); + expect(message).toContain("Brave LLM Context API error (429):"); + expect(message).not.toContain("tail-marker"); + expect(message.length).toBeLessThan(700); + }); + it("keeps Brave cache entries isolated by baseUrl", async () => { vi.stubEnv("BRAVE_API_KEY", ""); const mockFetch = vi.fn(async (_input?: unknown, _init?: unknown) => {