Skip to content
Merged
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
42 changes: 42 additions & 0 deletions apps/cli/src/__tests__/get-error-message.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { describe, expect, it } from "vitest";
import { getErrorMessage } from "../commands/helpers.js";

describe("getErrorMessage", () => {
it("unwraps the cause chain so connect errors survive fetch failed", () => {
const err = new TypeError("fetch failed", {
cause: new Error("connect EPERM 127.0.0.1:38886"),
});

expect(getErrorMessage(err)).toBe(
"fetch failed: connect EPERM 127.0.0.1:38886",
);
});

it("unwraps every connection error in an aggregate cause", () => {
const err = new TypeError("fetch failed", {
cause: new AggregateError([
new Error("connect EPERM ::1:38886"),
new Error("connect ECONNREFUSED 127.0.0.1:38886"),
]),
});

expect(getErrorMessage(err)).toBe(
"fetch failed: connect EPERM ::1:38886: connect ECONNREFUSED 127.0.0.1:38886",
);
});

it("returns the message unchanged without a cause", () => {
expect(getErrorMessage(new Error("plain"))).toBe("plain");
});

it("stringifies non-Error values", () => {
expect(getErrorMessage("boom")).toBe("boom");
});

it("stops on a cyclic cause chain", () => {
const err = new Error("loop");
err.cause = err;

expect(getErrorMessage(err)).toBe("loop");
});
});
35 changes: 33 additions & 2 deletions apps/cli/src/commands/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,39 @@ export async function confirmDestructiveAction(
}

export function getErrorMessage(err: unknown): string {
if (err instanceof Error) return err.message;
return String(err);
if (!(err instanceof Error)) return String(err);
// Node's fetch says "fetch failed" and keeps the actionable socket errors
// under `cause`. Multi-address connections use an AggregateError, so walk
// both links while guarding against malformed cyclic error graphs.
const seen = new Set<Error>();
const messages: string[] = [];
const pending: Error[] = [err];

while (pending.length > 0) {
const current = pending.pop();
if (current === undefined || seen.has(current)) continue;
seen.add(current);

if (current.message.length > 0) {
messages.push(current.message);
}

const children: Error[] = [];
if (current.cause instanceof Error) {
children.push(current.cause);
}
if (current instanceof AggregateError) {
children.push(
...current.errors.filter(
(nested): nested is Error => nested instanceof Error,
),
);
}
for (let index = children.length - 1; index >= 0; index -= 1) {
pending.push(children[index]);
}
}
return messages.join(": ");
}

export function parseReasoningLevel(
Expand Down
Loading