Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 107 additions & 1 deletion extensions/cli/src/util/formatError.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { formatError, formatAnthropicError } from "./formatError.js";
import {
formatError,
formatAnthropicError,
extractNestedJsonMessage,
} from "./formatError.js";

describe("formatError", () => {
it("should format Error objects correctly", () => {
Expand Down Expand Up @@ -135,6 +139,108 @@ describe("formatError", () => {
});
});

/**
* Real shape from continuedev/continue#12945: an SDK error message that is
* a JSON envelope whose error.message is ITSELF a pretty-printed JSON
* string, hiding the actual cause two parse levels deep. Shared vector for
* the formatError and extractNestedJsonMessage suites.
*/
function quotaErrorMessageVector(): string {
const googleBody = JSON.stringify(
{
error: {
code: 429,
message:
"You exceeded your current quota, please check your plan and billing details. Please retry in 45.191226092s.",
status: "RESOURCE_EXHAUSTED",
},
},
null,
2,
);
return JSON.stringify({
error: {
message: `${googleBody}\n`,
code: 429,
status: "Too Many Requests",
},
});
}

describe("formatError nested Gemini-style JSON messages", () => {
it("extracts the innermost message from a double-nested JSON error", () => {
const error = new Error(quotaErrorMessageVector());
expect(formatError(error)).toBe(
"You exceeded your current quota, please check your plan and billing details. Please retry in 45.191226092s.",
);
});

it("extracts a single-level nested message", () => {
const error = new Error(
JSON.stringify({
error: { message: "Quota exceeded", status: "RESOURCE_EXHAUSTED" },
}),
);
expect(formatError(error)).toBe("Quota exceeded");
});

it("leaves a message-less nested error unchanged", () => {
const raw = JSON.stringify({ error: { status: "RESOURCE_EXHAUSTED" } });
expect(formatError(new Error(raw))).toBe(raw);
});

it("leaves a primitive-valued error field unchanged", () => {
const raw = JSON.stringify({ error: "Invalid API key" });
expect(formatError(new Error(raw))).toBe(raw);
});

it("leaves malformed JSON unchanged", () => {
expect(formatError(new Error("{invalid json"))).toBe("{invalid json");
});

it("leaves plain non-JSON messages unchanged", () => {
expect(formatError(new Error("socket hang up"))).toBe("socket hang up");
});
});

describe("extractNestedJsonMessage (direct vectors)", () => {
it("extracts the innermost message from the double-nested shape", () => {
expect(extractNestedJsonMessage(quotaErrorMessageVector())).toBe(
"You exceeded your current quota, please check your plan and billing details. Please retry in 45.191226092s.",
);
});

it("extracts a single-level nested message", () => {
expect(
extractNestedJsonMessage(
JSON.stringify({ error: { message: "Quota exceeded" } }),
),
).toBe("Quota exceeded");
});

it("returns undefined for message-less JSON", () => {
expect(
extractNestedJsonMessage(
JSON.stringify({ error: { status: "RESOURCE_EXHAUSTED" } }),
),
).toBeUndefined();
});

it("returns undefined for a primitive error field", () => {
expect(
extractNestedJsonMessage(JSON.stringify({ error: "Invalid API key" })),
).toBeUndefined();
});

it("returns undefined for malformed JSON", () => {
expect(extractNestedJsonMessage("{invalid json")).toBeUndefined();
});

it("returns undefined for plain non-JSON text", () => {
expect(extractNestedJsonMessage("socket hang up")).toBeUndefined();
});
});
Comment on lines +239 to +242

describe("formatAnthropicError", () => {
it("should format invalid API key authentication errors", () => {
const error = new Error(
Expand Down
54 changes: 53 additions & 1 deletion extensions/cli/src/util/formatError.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,61 @@
type JsonObject = Record<string, unknown>;

function asJsonObject(value: unknown): JsonObject | undefined {
return typeof value === "object" && value !== null && !Array.isArray(value)
? (value as JsonObject)
: undefined;
}

/**
* Extract the human-readable message nested inside a provider error blob.
*
* Gemini-style errors arrive as a JSON envelope ({ error: { message, code,
* status } }) whose error.message is often ITSELF a JSON string (see
* continuedev/continue#12945) — without extraction users see raw JSON or
* "Unknown error". Walks the nesting to the innermost message; returns
* undefined for non-JSON, malformed, or message-less input so callers keep
* the original text. Mirrors extractNestedGeminiError in
* packages/openai-adapters/src/apis/Gemini.ts (kept as a small local mirror
* with shared test vectors rather than a new cross-package export).
*/
export function extractNestedJsonMessage(raw: string): string | undefined {
// Bound the unwrap depth so a gateway returning deeply nested error
// envelopes cannot force unbounded sequential parses.
const MAX_DEPTH = 8;
let node: unknown;
try {
node = JSON.parse(raw);
} catch {
return undefined;
}

let message: string | undefined;
for (let depth = 0; depth < MAX_DEPTH; depth++) {
const obj = asJsonObject(node);
if (!obj) {
break;
}
const target = asJsonObject(obj.error) ?? obj;
if (typeof target.message !== "string") {
break;
}
message = target.message;
try {
node = JSON.parse(target.message.trim());
} catch {
break;
}
}

return message?.trim();
}

/**
* Safely formats an error object into a readable string
*/
export function formatError(error: any): string {
if (error instanceof Error) {
return error.message;
return extractNestedJsonMessage(error.message) ?? error.message;
}

Comment on lines 56 to 60
if (typeof error === "string") {
Expand Down
Loading