@@ -20,6 +20,7 @@ import {
|
20 | 20 | wrapWebContent, |
21 | 21 | writeCachedSearchPayload, |
22 | 22 | } from "openclaw/plugin-sdk/provider-web-search"; |
| 23 | +import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime"; |
23 | 24 | import { |
24 | 25 | normalizeOptionalLowercaseString, |
25 | 26 | normalizeOptionalString, |
@@ -30,6 +31,10 @@ const EXA_SEARCH_TYPES = ["auto", "neural", "fast", "deep", "deep-reasoning", "i
|
30 | 31 | const EXA_FRESHNESS_VALUES = ["day", "week", "month", "year"] as const; |
31 | 32 | const EXA_MAX_SEARCH_COUNT = 100; |
32 | 33 | const EXA_ERROR_BODY_LIMIT_BYTES = 8 * 1024; |
| 34 | +// Exa search responses are untrusted external bodies. Cap the success JSON the |
| 35 | +// same way other bundled providers do (16 MiB) so a misbehaving or hostile |
| 36 | +// endpoint cannot stream an unbounded body into memory before we parse it. |
| 37 | +const EXA_SEARCH_JSON_MAX_BYTES = 16 * 1024 * 1024; |
33 | 38 | |
34 | 39 | type ExaConfig = { |
35 | 40 | apiKey?: string; |
@@ -70,9 +75,17 @@ type ExaSearchResponse = {
|
70 | 75 | results?: unknown; |
71 | 76 | }; |
72 | 77 | |
73 | | -async function readExaSearchResults(response: Response): Promise<ExaSearchResult[]> { |
| 78 | +async function readExaSearchResults( |
| 79 | +response: Response, |
| 80 | +opts?: { maxBytes?: number }, |
| 81 | +): Promise<ExaSearchResult[]> { |
| 82 | +const maxBytes = opts?.maxBytes ?? EXA_SEARCH_JSON_MAX_BYTES; |
| 83 | +const bytes = await readResponseWithLimit(response, maxBytes, { |
| 84 | +onOverflow: ({ maxBytes: maxBytesLocal }) => |
| 85 | +new Error(`Exa API response exceeds ${maxBytesLocal} bytes`), |
| 86 | +}); |
74 | 87 | try { |
75 | | -return normalizeExaResults(await response.json()); |
| 88 | +return normalizeExaResults(JSON.parse(new TextDecoder().decode(bytes))); |
76 | 89 | } catch (cause) { |
77 | 90 | throw new Error("Exa API returned malformed JSON", { cause }); |
78 | 91 | } |
|