diff --git a/extensions/exa/src/exa-web-search-provider.runtime.ts b/extensions/exa/src/exa-web-search-provider.runtime.ts index 56628d7593ad..9c25cf2f8c7c 100644 --- a/extensions/exa/src/exa-web-search-provider.runtime.ts +++ b/extensions/exa/src/exa-web-search-provider.runtime.ts @@ -85,7 +85,7 @@ async function readExaSearchResults( new Error(`Exa API response exceeds ${maxBytesLocal} bytes`), }); try { - return normalizeExaResults(JSON.parse(new TextDecoder().decode(bytes))); + return normalizeExaResults(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes))); } catch (cause) { throw new Error("Exa API returned malformed JSON", { cause }); } diff --git a/extensions/exa/src/exa-web-search-provider.test.ts b/extensions/exa/src/exa-web-search-provider.test.ts index e2021867dfbf..9c11e36b7b35 100644 --- a/extensions/exa/src/exa-web-search-provider.test.ts +++ b/extensions/exa/src/exa-web-search-provider.test.ts @@ -318,6 +318,21 @@ describe("exa web search provider", () => { ); }); + it("rejects invalid UTF-8 in Exa search JSON", async () => { + const prefix = new TextEncoder().encode( + '{"results":[{"url":"https://example.com","title":"bad', + ); + const suffix = new TextEncoder().encode('"}]}'); + const body = new Uint8Array(prefix.length + 1 + suffix.length); + body.set(prefix); + body[prefix.length] = 0xff; + body.set(suffix, prefix.length + 1); + + await expect(testing.readExaSearchResults(new Response(body))).rejects.toThrow( + "Exa API returned malformed JSON", + ); + }); + it("parses well-formed Exa search JSON under the byte cap", async () => { const response = new Response( JSON.stringify({ results: [{ url: "https://example.com", title: "Example" }] }),