Skip to content

Commit 24c8d08

Browse files
solodmdclaude
andcommitted
fix(infra): decode bounded JSON with TextDecoder to preserve BOM semantics
JSON.parse(buffer.toString('utf8')) retains a UTF-8 BOM, while Response.json() strips it. A provider usage response with a BOM prefix would be rejected as malformed. Use new TextDecoder().decode() to match the sibling bounded-read pattern in provider-http-errors.ts. Co-Authored-By: Claude <[email protected]>
1 parent 2f30572 commit 24c8d08

2 files changed

Lines changed: 21 additions & 1 deletion

File tree

src/infra/provider-usage.fetch.shared.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,26 @@ describe("provider usage fetch shared helpers", () => {
163163
});
164164
});
165165

166+
it("parses UTF-8 BOM-prefixed JSON that response.json() would accept", async () => {
167+
// \xEF\xBB\xBF is the UTF-8 BOM; response.json() strips it before
168+
// JSON.parse, but Buffer.toString("utf8") preserves it. The bounded
169+
// reader must use TextDecoder to match response.json() BOM semantics.
170+
const bom = new Uint8Array([0xef, 0xbb, 0xbf]);
171+
const json = new TextEncoder().encode(JSON.stringify({ windows: [] }));
172+
const combined = new Uint8Array(bom.length + json.length);
173+
combined.set(bom);
174+
combined.set(json, bom.length);
175+
const response = new Response(combined, {
176+
status: 200,
177+
headers: { "Content-Type": "application/json" },
178+
});
179+
const result = await readUsageJson("anthropic", response);
180+
expect(result).toEqual({
181+
ok: true,
182+
data: { windows: [] },
183+
});
184+
});
185+
166186
it("rejects an oversized JSON response", async () => {
167187
let pullCount = 0;
168188
const cancel = vi.fn(async () => undefined);

src/infra/provider-usage.fetch.shared.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export async function readUsageJson(
7575
onOverflow: ({ size }) =>
7676
new Error(`Provider ${provider} usage response too large: ${size} bytes`),
7777
});
78-
return { ok: true, data: JSON.parse(buffer.toString("utf8")) };
78+
return { ok: true, data: JSON.parse(new TextDecoder().decode(buffer)) };
7979
} catch {
8080
return {
8181
ok: false,

0 commit comments

Comments
 (0)