Skip to content

Commit bc88271

Browse files
claudiacodacyclaude
andcommitted
fix: normalize errors entries so reporting an API error cannot throw
`apiErrorDetails` pushed an `errors` entry's `message` through unconverted. A body like `{"errors": [{"message": 42}]}` therefore left a number in the list, and `formatError`'s `.trim()` threw on it — an exception raised while reporting an API error, which loses the error the user was waiting to read. Every entry now leaves `errorEntryDetail` as a string, and the ones with nothing to say are dropped rather than rendered as "undefined". Codacy also had the function at CCN 15 (limit 8). The body/array normalization and the serialized fallback come out as `objectDetails`, `errorEntryDetail` and `serializedBody`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent e94af33 commit bc88271

2 files changed

Lines changed: 63 additions & 17 deletions

File tree

‎src/utils/error.test.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,21 @@ describe("apiErrorDetails", () => {
3030
]);
3131
});
3232

33+
it("survives malformed errors entries instead of throwing over them", () => {
34+
// Nothing guarantees the shape of these — they are not in the spec. A
35+
// non-string `message` used to reach `formatError` unconverted and throw
36+
// on `.trim()`, losing the API error the user was waiting to read.
37+
expect(
38+
apiErrorDetails({
39+
errors: [{ message: 42 }, { message: null }, null, 7, [], undefined],
40+
}),
41+
).toEqual(["42", '{"message":null}', "null", "7", "[]"]);
42+
43+
expect(() =>
44+
formatError(apiError(400, "Bad Request", { errors: [{ message: 42 }] })),
45+
).not.toThrow();
46+
});
47+
3348
it("falls back to the serialized body when nothing is recognizable", () => {
3449
expect(apiErrorDetails({ code: 7 })).toEqual(['{"code":7}']);
3550
});

‎src/utils/error.ts‎

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,30 +22,61 @@ import { sanitizeText } from "./sanitize";
2222
*/
2323
export function apiErrorDetails(body: unknown): string[] {
2424
if (body && typeof body === "object") {
25-
const details: string[] = [];
26-
const obj = body as Record<string, unknown>;
27-
if (typeof obj.message === "string") {
28-
details.push(obj.message);
29-
}
30-
if (Array.isArray(obj.errors)) {
31-
for (const e of obj.errors) {
32-
details.push(typeof e === "string" ? e : ((e as any)?.message ?? JSON.stringify(e)));
33-
}
34-
}
35-
if (details.length === 0) {
36-
const serialized = JSON.stringify(body);
37-
if (serialized !== "{}" && serialized !== "null") {
38-
details.push(serialized);
39-
}
40-
}
41-
return details;
25+
const details = objectDetails(body as Record<string, unknown>);
26+
if (details.length > 0) return details;
27+
return serializedBody(body);
4228
}
4329
if (typeof body === "string" && body.length > 0) {
4430
return [body];
4531
}
4632
return [];
4733
}
4834

35+
function objectDetails(obj: Record<string, unknown>): string[] {
36+
const details: string[] = [];
37+
if (typeof obj.message === "string") {
38+
details.push(obj.message);
39+
}
40+
if (Array.isArray(obj.errors)) {
41+
for (const entry of obj.errors) {
42+
const detail = errorEntryDetail(entry);
43+
if (detail) details.push(detail);
44+
}
45+
}
46+
return details;
47+
}
48+
49+
/**
50+
* One `errors` entry as a string, whatever shape it arrived in.
51+
*
52+
* The entries are not in the spec, so nothing guarantees them. A `message`
53+
* that is a number used to reach the caller unconverted and `.trim()` threw
54+
* there — an exception raised while reporting an API error, which loses the
55+
* error the user was waiting to read. Every entry leaves here as a string, or
56+
* as `""` for the ones with nothing to say (`undefined`, and the functions and
57+
* symbols `JSON.stringify` declines to serialize).
58+
*/
59+
function errorEntryDetail(entry: unknown): string {
60+
if (typeof entry === "string") return entry;
61+
62+
const message = (entry as { message?: unknown } | null | undefined)?.message;
63+
if (typeof message === "string") return message;
64+
if (typeof message === "number" || typeof message === "boolean") {
65+
return String(message);
66+
}
67+
68+
const serialized = JSON.stringify(entry);
69+
return typeof serialized === "string" ? serialized : "";
70+
}
71+
72+
function serializedBody(body: unknown): string[] {
73+
const serialized = JSON.stringify(body);
74+
if (serialized && serialized !== "{}" && serialized !== "null") {
75+
return [serialized];
76+
}
77+
return [];
78+
}
79+
4980
/**
5081
* The line a failed command prints.
5182
*

0 commit comments

Comments
 (0)