Skip to content

Commit 79691d4

Browse files
committed
fix(provider): bound binary response reads
1 parent 6a2ccbc commit 79691d4

2 files changed

Lines changed: 51 additions & 1 deletion

File tree

src/agents/provider-http-errors.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,35 @@ import {
66
extractProviderErrorInfo,
77
extractProviderRequestId,
88
ProviderHttpError,
9+
readProviderBinaryResponse,
910
readProviderJsonResponse,
1011
} from "./provider-http-errors.js";
1112

13+
function createStreamingBinaryResponse(params: {
14+
chunkCount: number;
15+
chunkSize: number;
16+
byte: number;
17+
}): { response: Response; getReadCount: () => number } {
18+
let reads = 0;
19+
const stream = new ReadableStream<Uint8Array>({
20+
pull(controller) {
21+
if (reads >= params.chunkCount) {
22+
controller.close();
23+
return;
24+
}
25+
reads += 1;
26+
controller.enqueue(new Uint8Array(params.chunkSize).fill(params.byte));
27+
},
28+
});
29+
return {
30+
response: new Response(stream, {
31+
status: 200,
32+
headers: { "Content-Type": "audio/mpeg" },
33+
}),
34+
getReadCount: () => reads,
35+
};
36+
}
37+
1238
describe("provider error utils", () => {
1339
it("formats nested provider error details with request ids", async () => {
1440
const response = new Response(
@@ -147,4 +173,20 @@ describe("provider error utils", () => {
147173
"Provider catalog failed: malformed JSON response",
148174
);
149175
});
176+
177+
it("caps successful binary responses instead of buffering oversized bodies", async () => {
178+
const streamed = createStreamingBinaryResponse({
179+
chunkCount: 20,
180+
chunkSize: 1024,
181+
byte: 121,
182+
});
183+
184+
await expect(
185+
readProviderBinaryResponse(streamed.response, "Provider TTS failed", "audio", {
186+
maxBytes: 2048,
187+
}),
188+
).rejects.toThrow("Provider TTS failed: audio response exceeds 2048 bytes");
189+
190+
expect(streamed.getReadCount()).toBeLessThan(20);
191+
});
150192
});

src/agents/provider-http-errors.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
export { asFiniteNumber } from "../shared/number-coercion.js";
22
import { redactSensitiveText } from "../logging/redact.js";
3+
import { readResponseWithLimit } from "../media/read-response-with-limit.js";
34
import { normalizeOptionalString as trimToUndefined } from "../shared/string-coerce.js";
45
export { asBoolean } from "../utils/boolean.js";
56
export { normalizeOptionalString as trimToUndefined } from "../shared/string-coerce.js";
67

78
const ERROR_BODY_METADATA_LIMIT = 500;
9+
const PROVIDER_BINARY_RESPONSE_MAX_BYTES = 16 * 1024 * 1024;
810

911
export function asObject(value: unknown): Record<string, unknown> | undefined {
1012
return typeof value === "object" && value !== null && !Array.isArray(value)
@@ -321,9 +323,15 @@ export async function readProviderBinaryResponse(
321323
response: Response,
322324
label: string,
323325
kind = "binary",
326+
opts?: {
327+
maxBytes?: number;
328+
},
324329
): Promise<Uint8Array> {
325330
assertProviderBinaryResponseContent(response, label, kind);
326-
const bytes = new Uint8Array(await response.arrayBuffer());
331+
const maxBytes = opts?.maxBytes ?? PROVIDER_BINARY_RESPONSE_MAX_BYTES;
332+
const bytes = await readResponseWithLimit(response, maxBytes, {
333+
onOverflow: ({ maxBytes }) => new Error(`${label}: ${kind} response exceeds ${maxBytes} bytes`),
334+
});
327335
if (bytes.byteLength === 0) {
328336
throw new Error(`${label}: malformed ${kind} response`);
329337
}

0 commit comments

Comments
 (0)