fix(agents): bound native pdf error bodies

This commit is contained in:
Vincent Koc
2026-05-28 06:39:45 +02:00
parent 647e18aa04
commit c841218ace
2 changed files with 116 additions and 4 deletions

View File

@@ -112,6 +112,65 @@ describe("native PDF provider API calls", () => {
).rejects.toThrow("Anthropic PDF request failed");
});
it("bounds large Anthropic API error bodies", async () => {
let canceled = false;
const body = new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(new TextEncoder().encode(`${"x".repeat(9_000)}tail-marker`));
},
cancel() {
canceled = true;
},
});
mockFetchResponse(
new Response(body, {
status: 400,
statusText: "Bad Request",
}),
);
const error = await pdfNativeProviders
.anthropicAnalyzePdf(makeAnthropicAnalyzeParams())
.catch((caught: unknown) => caught as Error);
expect(error).toBeInstanceOf(Error);
expect(error.message).toContain("Anthropic PDF request failed");
expect(error.message).not.toContain("tail-marker");
expect(error.message.length).toBeLessThan(500);
expect(canceled).toBe(true);
});
it("cancels Anthropic API error bodies that exactly fill the byte cap", async () => {
let canceled = false;
const body = new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(new TextEncoder().encode("x".repeat(8 * 1024)));
},
cancel() {
canceled = true;
},
});
mockFetchResponse(
new Response(body, {
status: 400,
statusText: "Bad Request",
}),
);
const error = await Promise.race([
pdfNativeProviders
.anthropicAnalyzePdf(makeAnthropicAnalyzeParams())
.catch((caught: unknown) => caught as Error),
new Promise<Error>((_resolve, reject) => {
setTimeout(() => reject(new Error("timed out waiting for bounded error body")), 500);
}),
]);
expect(error).toBeInstanceOf(Error);
expect(error.message).toContain("Anthropic PDF request failed");
expect(canceled).toBe(true);
});
it("anthropicAnalyzePdf throws when response has no text", async () => {
mockFetchResponse({
ok: true,

View File

@@ -13,6 +13,59 @@ type PdfInput = {
};
const NATIVE_PDF_PROVIDER_FETCH_TIMEOUT_MS = 120_000;
const NATIVE_PDF_ERROR_BODY_MAX_BYTES = 8 * 1024;
const NATIVE_PDF_ERROR_BODY_MAX_CHARS = 400;
async function readErrorBodySnippet(res: Response): Promise<string> {
try {
const body = res.body;
if (!body || typeof body.getReader !== "function") {
return (await res.text()).slice(0, NATIVE_PDF_ERROR_BODY_MAX_CHARS);
}
const reader = body.getReader();
const chunks: Uint8Array[] = [];
let total = 0;
let truncated = false;
try {
while (true) {
const { done, value } = await reader.read();
if (done || !value?.byteLength) {
break;
}
const remaining = NATIVE_PDF_ERROR_BODY_MAX_BYTES - total;
if (remaining <= 0) {
truncated = true;
break;
}
if (value.byteLength > remaining) {
chunks.push(value.subarray(0, remaining));
total += remaining;
truncated = true;
break;
}
chunks.push(value);
total += value.byteLength;
if (total >= NATIVE_PDF_ERROR_BODY_MAX_BYTES) {
truncated = true;
break;
}
}
} finally {
if (truncated) {
await reader.cancel().catch(() => undefined);
}
try {
reader.releaseLock();
} catch {}
}
return new TextDecoder()
.decode(Buffer.concat(chunks, total))
.slice(0, NATIVE_PDF_ERROR_BODY_MAX_CHARS);
} catch {
return "";
}
}
// ---------------------------------------------------------------------------
// Anthropic native PDF via Messages API
@@ -80,9 +133,9 @@ export async function anthropicAnalyzePdf(params: {
});
if (!res.ok) {
const body = await res.text().catch(() => "");
const body = await readErrorBodySnippet(res);
throw new Error(
`Anthropic PDF request failed (${res.status} ${res.statusText})${body ? `: ${body.slice(0, 400)}` : ""}`,
`Anthropic PDF request failed (${res.status} ${res.statusText})${body ? `: ${body}` : ""}`,
);
}
@@ -165,9 +218,9 @@ export async function geminiAnalyzePdf(params: {
});
if (!res.ok) {
const body = await res.text().catch(() => "");
const body = await readErrorBodySnippet(res);
throw new Error(
`Gemini PDF request failed (${res.status} ${res.statusText})${body ? `: ${body.slice(0, 400)}` : ""}`,
`Gemini PDF request failed (${res.status} ${res.statusText})${body ? `: ${body}` : ""}`,
);
}