mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(ai): treat only plain objects as provider error bodies (#7205)
pickBodyText serialized any non-empty object into the error body. SDK error
fields can hold class instances instead of parsed bodies - AWS SDK v3's
$response.body is an HTTP stream/response wrapper - and stringifying one
produced garbage like {"_events":...} as the body, which then replaced
error.message in the composed display string. error.message is where the SDK
puts the real deserialized exception text ("Input is too long...", schema
validation details), so the one useful string was discarded for noise.
The existing pipe-based stream sniffing only covers node-style streams; web
ReadableStreams (pipeTo/pipeThrough, no pipe) and non-stream wrapper classes
slipped through. Require Object.prototype/null prototype before serializing
a candidate body - parsed JSON bodies are plain objects by construction, so
real gateway bodies still surface.
This commit is contained in:
parent
66cd061ab4
commit
4523528b21
2 changed files with 72 additions and 4 deletions
|
|
@ -83,11 +83,11 @@ function extractBody(error: SdkErrorShape): string | undefined {
|
|||
|
||||
function pickBodyText(error: SdkErrorShape): string | undefined {
|
||||
if (typeof error.body === "string") return error.body;
|
||||
if (isNonEmptyObject(error.error)) return safeJsonStringify(error.error);
|
||||
if (isPlainNonEmptyObject(error.error)) return safeJsonStringify(error.error);
|
||||
const responseBody = error.$response?.body;
|
||||
if (typeof responseBody === "string") return responseBody;
|
||||
if (isReadableStreamLike(responseBody)) return undefined;
|
||||
if (isNonEmptyObject(responseBody)) return safeJsonStringify(responseBody);
|
||||
if (isPlainNonEmptyObject(responseBody)) return safeJsonStringify(responseBody);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
|
@ -95,8 +95,25 @@ function isReadableStreamLike(value: unknown): boolean {
|
|||
return typeof value === "object" && value !== null && "pipe" in value && typeof value.pipe === "function";
|
||||
}
|
||||
|
||||
function isNonEmptyObject(value: unknown): boolean {
|
||||
return typeof value === "object" && value !== null && Object.keys(value).length > 0;
|
||||
/**
|
||||
* Only a PLAIN object counts as an HTTP body. SDK error fields can hold class
|
||||
* instances instead of parsed bodies — AWS SDK v3's `$response.body` is an
|
||||
* HTTP stream/response wrapper object, and stringifying one produced garbage
|
||||
* like `{"_events":...}` as the "body", which then REPLACED `error.message`
|
||||
* in the composed display string. `error.message` is where the SDK puts the
|
||||
* real deserialized exception text ("Input is too long...", schema validation
|
||||
* details, ...), so the one useful string was discarded for noise. A class
|
||||
* instance yields no body, `messageCarriesBody` stays true, and the real
|
||||
* message survives. Complements the `pipe` sniffing above: web
|
||||
* ReadableStreams (pipeTo/pipeThrough, no `pipe`) and non-stream SDK wrapper
|
||||
* classes fail the prototype check, while parsed JSON bodies (plain objects
|
||||
* by construction) still pass.
|
||||
*/
|
||||
function isPlainNonEmptyObject(value: unknown): boolean {
|
||||
if (typeof value !== "object" || value === null) return false;
|
||||
const proto = Object.getPrototypeOf(value);
|
||||
if (proto !== Object.prototype && proto !== null) return false;
|
||||
return Object.keys(value).length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -85,6 +85,57 @@ describe("normalizeProviderError", () => {
|
|||
expect(norm.messageCarriesBody).toBe(true);
|
||||
});
|
||||
|
||||
it("ignores a class-instance response body without a pipe method instead of serializing it", () => {
|
||||
// Not every SDK response wrapper is a node stream: web ReadableStreams
|
||||
// and SDK-specific wrapper classes have no `pipe`, but serializing them
|
||||
// still yields internals-noise that would replace the real message.
|
||||
class SdkHttpResponseBody {
|
||||
locked = false;
|
||||
state = { storedError: undefined };
|
||||
}
|
||||
const error = Object.assign(new Error("Input is too long for requested model."), {
|
||||
name: "ValidationException",
|
||||
$metadata: { httpStatusCode: 400 },
|
||||
$response: { statusCode: 400, body: new SdkHttpResponseBody() },
|
||||
});
|
||||
|
||||
const norm = normalizeProviderError(error);
|
||||
|
||||
expect(norm.status).toBe(400);
|
||||
expect(norm.body).toBeUndefined();
|
||||
expect(norm.message).toContain("Input is too long");
|
||||
expect(norm.messageCarriesBody).toBe(true);
|
||||
});
|
||||
|
||||
it("ignores a class-instance `error` field instead of serializing it", () => {
|
||||
class SdkInnerError {
|
||||
code = "EPROTO";
|
||||
internalState = {};
|
||||
}
|
||||
const error = Object.assign(new Error("TLS handshake failed"), {
|
||||
status: 502,
|
||||
error: new SdkInnerError(),
|
||||
});
|
||||
|
||||
const norm = normalizeProviderError(error);
|
||||
|
||||
expect(norm.body).toBeUndefined();
|
||||
expect(norm.message).toBe("TLS handshake failed");
|
||||
expect(norm.messageCarriesBody).toBe(true);
|
||||
});
|
||||
|
||||
it("still surfaces a plain parsed JSON body object", () => {
|
||||
const error = Object.assign(new Error("400 status code (no body)"), {
|
||||
status: 400,
|
||||
error: { message: "schema validation failed", field: "tools[0]" },
|
||||
});
|
||||
|
||||
const norm = normalizeProviderError(error);
|
||||
|
||||
expect(norm.body).toBe('{"message":"schema validation failed","field":"tools[0]"}');
|
||||
expect(norm.messageCarriesBody).toBe(false);
|
||||
});
|
||||
|
||||
it("JSON-stringifies a non-Error thrown value", () => {
|
||||
const norm = normalizeProviderError({ reason: "boom" });
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue