Skip to content

Commit 16f6145

Browse files
committed
fix(infra): enforce maxBytes in body-less HTTP error snippet path
1 parent 21be45c commit 16f6145

2 files changed

Lines changed: 109 additions & 1 deletion

File tree

src/infra/http-error-body.test.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { describe, expect, it } from "vitest";
2+
import { readResponseBodySnippet } from "./http-error-body.js";
3+
4+
function bodyLessResponse(text: string): Response {
5+
return {
6+
body: null,
7+
text: async () => text,
8+
} as unknown as Response;
9+
}
10+
11+
describe("readResponseBodySnippet", () => {
12+
it("returns full text when under both limits (body-less path)", async () => {
13+
const text = "short text";
14+
const result = await readResponseBodySnippet(bodyLessResponse(text), {
15+
maxBytes: 1024,
16+
maxChars: 50,
17+
});
18+
expect(result).toBe(text);
19+
});
20+
21+
it("truncates by maxChars when under maxBytes (body-less path)", async () => {
22+
const text = "abcdefghij";
23+
const result = await readResponseBodySnippet(bodyLessResponse(text), {
24+
maxBytes: 1024,
25+
maxChars: 5,
26+
});
27+
expect(result).toBe("abcde");
28+
});
29+
30+
it("truncates by maxBytes in the body-less path", async () => {
31+
const text = "a".repeat(200);
32+
const result = await readResponseBodySnippet(bodyLessResponse(text), {
33+
maxBytes: 50,
34+
maxChars: 500,
35+
});
36+
const byteLen = new TextEncoder().encode(result).length;
37+
expect(byteLen).toBeLessThanOrEqual(50);
38+
expect(result.length).toBeLessThan(text.length);
39+
});
40+
41+
it("enforces maxBytes before maxChars in the body-less path", async () => {
42+
const text = "a".repeat(500);
43+
const result = await readResponseBodySnippet(bodyLessResponse(text), {
44+
maxBytes: 30,
45+
maxChars: 500,
46+
});
47+
const byteLen = new TextEncoder().encode(result).length;
48+
expect(byteLen).toBeLessThanOrEqual(30);
49+
});
50+
51+
it("does not split multi-byte UTF-8 characters at the byte boundary", async () => {
52+
// U+1F600 (😀) is 4 bytes in UTF-8: F0 9F 98 80
53+
const text = "ab😀cd";
54+
// 2 ASCII bytes (ab) + cut before the 4-byte emoji
55+
const result = await readResponseBodySnippet(bodyLessResponse(text), {
56+
maxBytes: 3,
57+
maxChars: 100,
58+
});
59+
// With stream:true, incomplete multi-byte sequence is dropped
60+
expect(result).toBe("ab");
61+
});
62+
63+
it("stream path still enforces maxBytes", async () => {
64+
const data = new Uint8Array(500).fill(97); // 500 'a' bytes
65+
const response = new Response(new Blob([data]).stream());
66+
const result = await readResponseBodySnippet(response, {
67+
maxBytes: 100,
68+
maxChars: 500,
69+
});
70+
const byteLen = new TextEncoder().encode(result).length;
71+
expect(byteLen).toBeLessThanOrEqual(100);
72+
});
73+
74+
it("stream path still enforces maxChars", async () => {
75+
const data = new Uint8Array(500).fill(97);
76+
const response = new Response(new Blob([data]).stream());
77+
const result = await readResponseBodySnippet(response, {
78+
maxBytes: 200,
79+
maxChars: 10,
80+
});
81+
expect(result.length).toBeLessThanOrEqual(10);
82+
});
83+
84+
it("returns empty string when maxBytes is 0 (body-less path)", async () => {
85+
const result = await readResponseBodySnippet(bodyLessResponse("some text"), {
86+
maxBytes: 0,
87+
maxChars: 100,
88+
});
89+
expect(result).toBe("");
90+
});
91+
92+
it("returns empty string for empty response body", async () => {
93+
const result = await readResponseBodySnippet(bodyLessResponse(""), {
94+
maxBytes: 1024,
95+
maxChars: 50,
96+
});
97+
expect(result).toBe("");
98+
});
99+
});

src/infra/http-error-body.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,16 @@ export async function readResponseBodySnippet(
55
try {
66
const body = response.body;
77
if (!body || typeof body.getReader !== "function") {
8-
return (await response.text()).slice(0, limits.maxChars);
8+
const text = await response.text();
9+
const encoded = new TextEncoder().encode(text);
10+
if (encoded.byteLength > limits.maxBytes) {
11+
return new TextDecoder()
12+
.decode(encoded.subarray(0, limits.maxBytes), {
13+
stream: true,
14+
})
15+
.slice(0, limits.maxChars);
16+
}
17+
return text.slice(0, limits.maxChars);
918
}
1019

1120
const reader = body.getReader();

0 commit comments

Comments
 (0)