From f284ce3b4df717dde2bbec22f840906c31bcb1f0 Mon Sep 17 00:00:00 2001 From: cxbAsDev Date: Wed, 1 Jul 2026 01:00:17 +0800 Subject: [PATCH] fix(cli): bound docs search API response reads with committed test (#98188) --- src/commands/docs.test.ts | 29 +++++++++++++++++++++++++++++ src/commands/docs.ts | 7 ++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/commands/docs.test.ts b/src/commands/docs.test.ts index 68891433878d..17b69c2b5861 100644 --- a/src/commands/docs.test.ts +++ b/src/commands/docs.test.ts @@ -94,4 +94,33 @@ describe("docsSearchCommand", () => { expect(runtime.exit).not.toHaveBeenCalled(); expect(runtime.log).toHaveBeenCalled(); }); + + it("rejects oversized docs search responses", async () => { + const ONE_MIB = 1024 * 1024; + const cancel = vi.fn(); + const stream = new ReadableStream({ + cancel, + start(controller) { + for (let i = 0; i < 10; i++) { + controller.enqueue(new Uint8Array(ONE_MIB)); + } + controller.close(); + }, + }); + fetchMock.mockResolvedValueOnce( + new Response(stream, { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); + const runtime = makeRuntime(); + + await docsSearchCommand(["oversized"], runtime); + + expect(runtime.error).toHaveBeenCalledWith( + expect.stringContaining("Docs search response exceeds"), + ); + expect(runtime.exit).toHaveBeenCalledWith(1); + expect(cancel).toHaveBeenCalledOnce(); + }); }); diff --git a/src/commands/docs.ts b/src/commands/docs.ts index 2c6f4dca1407..fec7b94984b1 100644 --- a/src/commands/docs.ts +++ b/src/commands/docs.ts @@ -1,4 +1,5 @@ // Implements docs link/search output for `openclaw docs`. +import { readResponseWithLimit } from "@openclaw/media-core/read-response-with-limit"; import { formatDocsLink } from "../../packages/terminal-core/src/links.js"; import { isRich, theme } from "../../packages/terminal-core/src/theme.js"; import { formatCliCommand } from "../cli/command-format.js"; @@ -6,6 +7,7 @@ import type { RuntimeEnv } from "../runtime.js"; const SEARCH_API = "https://docs.openclaw.ai/api/search"; const SEARCH_TIMEOUT_MS = 30_000; +const DOCS_SEARCH_RESPONSE_MAX_BYTES = 8 * 1024 * 1024; type DocResult = { title: string; @@ -75,7 +77,10 @@ async function fetchDocsSearch(query: string): Promise { if (!response.ok) { throw new Error(`HTTP ${response.status}`); } - const payload = (await response.json()) as DocsSearchResponse; + const bytes = await readResponseWithLimit(response, DOCS_SEARCH_RESPONSE_MAX_BYTES, { + onOverflow: ({ maxBytes }) => new Error(`Docs search response exceeds ${maxBytes} bytes`), + }); + const payload = JSON.parse(new TextDecoder().decode(bytes)) as DocsSearchResponse; return parseDocsSearchResults(payload.results); } finally { clearTimeout(timeout);