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
157 changes: 156 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,157 @@ 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");
});

it("falls back to the raw message when the nested message is empty", () => {
// A blank extracted message is worse than the original envelope — the
// caller must keep the raw text rather than render an empty error.
const raw = JSON.stringify({ error: { message: "" } });
expect(formatError(new Error(raw))).toBe(raw);
});

it("extracts nested messages from raw string errors too", () => {
expect(formatError(quotaErrorMessageVector())).toBe(
"You exceeded your current quota, please check your plan and billing details. Please retry in 45.191226092s.",
);
});

it("extracts nested messages from plain-object message properties", () => {
expect(formatError({ message: quotaErrorMessageVector() })).toBe(
"You exceeded your current quota, please check your plan and billing details. Please retry in 45.191226092s.",
);
});
});

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();
});

it("returns undefined for an empty or whitespace-only nested message", () => {
expect(
extractNestedJsonMessage(JSON.stringify({ error: { message: "" } })),
).toBeUndefined();
expect(
extractNestedJsonMessage(JSON.stringify({ error: { message: " " } })),
).toBeUndefined();
});

/** nest(0) = "CORE"; nest(k) wraps nest(k-1) in one more JSON envelope. */
function nestedEnvelope(levels: number): string {
let raw = "CORE";
for (let i = 0; i < levels; i++) {
raw = JSON.stringify({ error: { message: raw } });
}
return raw;
}

it("fully unwraps envelopes nested within the depth cap", () => {
expect(extractNestedJsonMessage(nestedEnvelope(5))).toBe("CORE");
});

it("stops unwrapping at the depth cap on pathological nesting", () => {
// MAX_DEPTH = 8: a 10-level envelope stops with the level-2 envelope
// still intact — bounded work, never an unbounded parse chain.
expect(extractNestedJsonMessage(nestedEnvelope(10))).toBe(
nestedEnvelope(2),
);
});
});

describe("formatAnthropicError", () => {
it("should format invalid API key authentication errors", () => {
const error = new Error(
Expand Down
62 changes: 60 additions & 2 deletions extensions/cli/src/util/formatError.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,76 @@
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;
}
}

// A blank extracted message is worse than the original text — report
// "nothing found" so callers keep the raw envelope.
const trimmed = message?.trim();
return trimmed === undefined || trimmed === "" ? undefined : trimmed;
}

/**
* 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 59 to 63
if (typeof error === "string") {
return error;
return extractNestedJsonMessage(error) ?? error;
}

if (error && typeof error === "object") {
// Try to extract common error properties
if (error.message) {
if (typeof error.message === "string") {
return extractNestedJsonMessage(error.message) ?? error.message;
}
return error.message;
}
if (error.error) {
Expand Down
Loading