Skip to content

Commit 98ba327

Browse files
committed
fix(runtime): expose errno in fetch-failed JSON cause.code
1 parent 3b7c4cf commit 98ba327

4 files changed

Lines changed: 160 additions & 73 deletions

File tree

packages/runtime/src/error-handler.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,12 @@ function fromFetchFailed(err: TypeError): BailianError {
7878
if (causeMsg && causeMsg !== code) detailParts.push(causeMsg);
7979
const detail = detailParts.length > 0 ? detailParts.join(": ") : "unknown cause";
8080

81+
// Prefer the errno (ENOTFOUND, …) so JSON toJSON() exposes cause.code for agents.
8182
return new BailianError(
8283
`Network request failed: ${detail}`,
8384
ExitCode.NETWORK,
8485
pickNetworkHint(code),
85-
{ cause: err },
86+
{ cause: cause ?? err },
8687
);
8788
}
8889

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { ExitCode } from "bailian-cli-core";
2+
import { expect, test } from "vite-plus/test";
3+
import { handleError } from "../src/error-handler.ts";
4+
5+
test("handleError: fetch failed JSON includes cause.code from errno", () => {
6+
const previousOutput = process.env.DASHSCOPE_OUTPUT;
7+
process.env.DASHSCOPE_OUTPUT = "json";
8+
9+
let stderr = "";
10+
const originalWrite = process.stderr.write.bind(process.stderr);
11+
const originalExit = process.exit;
12+
process.stderr.write = ((chunk: string | Uint8Array) => {
13+
stderr += String(chunk);
14+
return true;
15+
}) as typeof process.stderr.write;
16+
process.exit = ((code?: number) => {
17+
throw new Error(`process.exit:${code ?? 0}`);
18+
}) as typeof process.exit;
19+
20+
const root = Object.assign(new Error("getaddrinfo ENOTFOUND example.invalid"), {
21+
code: "ENOTFOUND",
22+
});
23+
const fetchFailed = new TypeError("fetch failed", { cause: root });
24+
25+
try {
26+
expect(() => handleError(fetchFailed, "bl")).toThrow(
27+
new RegExp(`process\\.exit:${ExitCode.NETWORK}`),
28+
);
29+
const payload = JSON.parse(stderr.trim()) as {
30+
error: { code: number; message: string; cause?: { message: string; code?: string } };
31+
};
32+
expect(payload.error.code).toBe(ExitCode.NETWORK);
33+
expect(payload.error.message).toMatch(/ENOTFOUND/);
34+
expect(payload.error.cause).toEqual({
35+
message: root.message,
36+
code: "ENOTFOUND",
37+
});
38+
} finally {
39+
process.stderr.write = originalWrite;
40+
process.exit = originalExit;
41+
if (previousOutput === undefined) {
42+
delete process.env.DASHSCOPE_OUTPUT;
43+
} else {
44+
process.env.DASHSCOPE_OUTPUT = previousOutput;
45+
}
46+
}
47+
});
48+
49+
test("handleError: fetch failed without nested cause still maps to NETWORK", () => {
50+
const previousOutput = process.env.DASHSCOPE_OUTPUT;
51+
process.env.DASHSCOPE_OUTPUT = "json";
52+
53+
let stderr = "";
54+
const originalWrite = process.stderr.write.bind(process.stderr);
55+
const originalExit = process.exit;
56+
process.stderr.write = ((chunk: string | Uint8Array) => {
57+
stderr += String(chunk);
58+
return true;
59+
}) as typeof process.stderr.write;
60+
process.exit = ((code?: number) => {
61+
throw new Error(`process.exit:${code ?? 0}`);
62+
}) as typeof process.exit;
63+
64+
const fetchFailed = new TypeError("fetch failed");
65+
66+
try {
67+
expect(() => handleError(fetchFailed, "bl")).toThrow(
68+
new RegExp(`process\\.exit:${ExitCode.NETWORK}`),
69+
);
70+
const payload = JSON.parse(stderr.trim()) as {
71+
error: { code: number; message: string; cause?: { message: string; code?: string } };
72+
};
73+
expect(payload.error.code).toBe(ExitCode.NETWORK);
74+
expect(payload.error.message).toMatch(/unknown cause/);
75+
expect(payload.error.cause).toEqual({ message: "fetch failed" });
76+
} finally {
77+
process.stderr.write = originalWrite;
78+
process.exit = originalExit;
79+
if (previousOutput === undefined) {
80+
delete process.env.DASHSCOPE_OUTPUT;
81+
} else {
82+
process.env.DASHSCOPE_OUTPUT = previousOutput;
83+
}
84+
}
85+
});

0 commit comments

Comments
 (0)