62fad94f1a
Add a shared normalizeProviderError helper in packages/ai/src/utils/error-body.ts and route the 8 body-blind / status-only providers through it (amazon-bedrock, azure-openai-responses, google, google-vertex, images/openrouter, openai-codex-responses, openai-completions, openai-responses). Non-schema 4xx/5xx responses from proxies / gateways now show the real reason carried in the response body alongside the HTTP status, instead of "403 status code (no body)" or "Unknown: UnknownError". The helper probes status (statusCode, status, $metadata.httpStatusCode, $response.statusCode) and body (body, parsed error object, $response.body) across the Mistral, OpenAI, Google, and Bedrock SDK shapes, truncates the body at a 4000 char cap, and preserves error.message when the SDK already folded the body in (Anthropic / Google happy path). mistral.ts and anthropic.ts are left untouched. Provider prefixes and the OpenRouter metadata.raw append are preserved. closes #5763
155 lines
5.1 KiB
TypeScript
155 lines
5.1 KiB
TypeScript
// Unit tests for the shared provider error-body normalizer.
|
|
//
|
|
// See issues/provider-error-body-passthrough. These cover one synthesized error
|
|
// object per SDK shape (Mistral, openai APIError, @google/genai ApiError, AWS
|
|
// Bedrock ServiceException), plus the non-Error fallback, truncation, the empty
|
|
// parsed-body edge case, and the formatProviderError compose helper.
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
import { formatProviderError, MAX_PROVIDER_ERROR_BODY_CHARS, normalizeProviderError } from "../src/utils/error-body.ts";
|
|
|
|
describe("normalizeProviderError", () => {
|
|
it("extracts status and body from a Mistral-shaped error", () => {
|
|
const error = Object.assign(new Error("Mistral request failed"), {
|
|
statusCode: 403,
|
|
body: '{"error":"blocked by gateway WAF"}',
|
|
});
|
|
|
|
const norm = normalizeProviderError(error);
|
|
|
|
expect(norm.status).toBe(403);
|
|
expect(norm.body).toBe('{"error":"blocked by gateway WAF"}');
|
|
expect(norm.messageCarriesBody).toBe(false);
|
|
});
|
|
|
|
it("reads the parsed body off an openai APIError when the message is opaque", () => {
|
|
// makeMessage(status, error, message) yields "<status> status code (no body)"
|
|
// when the parsed body is unparsed, while the body stays on error.error.
|
|
const error = Object.assign(new Error("403 status code (no body)"), {
|
|
status: 403,
|
|
error: { error: "blocked by gateway WAF" },
|
|
});
|
|
|
|
const norm = normalizeProviderError(error);
|
|
|
|
expect(norm.status).toBe(403);
|
|
expect(norm.body).toBe('{"error":"blocked by gateway WAF"}');
|
|
expect(norm.messageCarriesBody).toBe(false);
|
|
});
|
|
|
|
it("preserves the message when @google/genai already folds the body into it", () => {
|
|
const body = { error: { code: 403, message: "Permission denied" } };
|
|
const error = Object.assign(new Error(JSON.stringify(body)), {
|
|
status: 403,
|
|
});
|
|
|
|
const norm = normalizeProviderError(error);
|
|
|
|
expect(norm.status).toBe(403);
|
|
expect(norm.messageCarriesBody).toBe(true);
|
|
expect(norm.message).toBe(JSON.stringify(body));
|
|
});
|
|
|
|
it("extracts status and body from a Bedrock-shaped ServiceException", () => {
|
|
const error = Object.assign(new Error("UnknownError"), {
|
|
name: "UnknownError",
|
|
$metadata: { httpStatusCode: 403 },
|
|
$response: { statusCode: 403, body: '{"message":"blocked by gateway WAF"}' },
|
|
});
|
|
|
|
const norm = normalizeProviderError(error);
|
|
|
|
expect(norm.status).toBe(403);
|
|
expect(norm.body).toBe('{"message":"blocked by gateway WAF"}');
|
|
expect(norm.messageCarriesBody).toBe(false);
|
|
});
|
|
|
|
it("JSON-stringifies a non-Error thrown value", () => {
|
|
const norm = normalizeProviderError({ reason: "boom" });
|
|
|
|
expect(norm.status).toBeUndefined();
|
|
expect(norm.body).toBeUndefined();
|
|
expect(norm.message).toBe('{"reason":"boom"}');
|
|
expect(norm.messageCarriesBody).toBe(false);
|
|
});
|
|
|
|
it("treats an empty parsed body object as no body", () => {
|
|
const error = Object.assign(new Error("403 status code (no body)"), {
|
|
status: 403,
|
|
error: {},
|
|
});
|
|
|
|
const norm = normalizeProviderError(error);
|
|
|
|
expect(norm.body).toBeUndefined();
|
|
expect(norm.messageCarriesBody).toBe(true);
|
|
});
|
|
|
|
it("truncates the body at the cap", () => {
|
|
const longBody = "x".repeat(MAX_PROVIDER_ERROR_BODY_CHARS + 50);
|
|
const error = Object.assign(new Error("failed"), {
|
|
statusCode: 500,
|
|
body: longBody,
|
|
});
|
|
|
|
const norm = normalizeProviderError(error);
|
|
|
|
expect(norm.body).toContain("... [truncated 50 chars]");
|
|
expect(norm.body?.length).toBeLessThan(longBody.length);
|
|
});
|
|
|
|
it("sets messageCarriesBody when the message already contains the extracted body", () => {
|
|
const error = Object.assign(new Error("500: upstream exploded"), {
|
|
statusCode: 500,
|
|
body: "upstream exploded",
|
|
});
|
|
|
|
const norm = normalizeProviderError(error);
|
|
|
|
expect(norm.messageCarriesBody).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("formatProviderError", () => {
|
|
it("surfaces status and body without a prefix", () => {
|
|
const norm = normalizeProviderError(
|
|
Object.assign(new Error("403 status code (no body)"), {
|
|
status: 403,
|
|
error: { error: "blocked by gateway WAF" },
|
|
}),
|
|
);
|
|
|
|
const formatted = formatProviderError(norm);
|
|
|
|
expect(formatted).toContain("403");
|
|
expect(formatted).toContain("blocked by gateway WAF");
|
|
expect(formatted).not.toBe("403 status code (no body)");
|
|
});
|
|
|
|
it("applies a provider prefix with status and body", () => {
|
|
const norm = normalizeProviderError(
|
|
Object.assign(new Error("403 status code (no body)"), {
|
|
status: 403,
|
|
error: { error: "blocked by gateway WAF" },
|
|
}),
|
|
);
|
|
|
|
expect(formatProviderError(norm, "OpenAI API error")).toBe(
|
|
'OpenAI API error (403): {"error":"blocked by gateway WAF"}',
|
|
);
|
|
});
|
|
|
|
it("preserves the message (with prefix + status) when it already carries the body", () => {
|
|
const body = JSON.stringify({ error: { message: "Permission denied" } });
|
|
const norm = normalizeProviderError(Object.assign(new Error(body), { status: 403 }));
|
|
|
|
expect(formatProviderError(norm, "OpenAI API error")).toBe(`OpenAI API error (403): ${body}`);
|
|
});
|
|
|
|
it("returns the bare message for a non-Error value", () => {
|
|
const norm = normalizeProviderError({ reason: "boom" });
|
|
|
|
expect(formatProviderError(norm)).toBe('{"reason":"boom"}');
|
|
});
|
|
});
|