fix(gateway): treat all fetch failed TypeErrors as transient

When a TypeError("fetch failed") from undici had a cause with an
unrecognized error code, isTransientNetworkError() would recurse into
the cause, fail to match, and return false — causing the gateway to
exit via process.exit(1).

Since the outer TypeError("fetch failed") already indicates a
network-level failure, treat it as transient unconditionally regardless
of the cause chain contents.

Added tests for fetch failures with unrecognized cause and no cause.

Fixes #4425
This commit is contained in:
Ayush Ojha 2026-01-30 00:11:34 -08:00
parent 9025da2296
commit 4f3b78aa5e
2 changed files with 29 additions and 3 deletions

View File

@ -114,6 +114,32 @@ describe("installUnhandledRejectionHandler - fatal detection", () => {
);
});
it("does NOT exit on undici fetch failures with unrecognized cause", () => {
const fetchErr = Object.assign(new TypeError("fetch failed"), {
cause: new Error("some unknown undici error"),
});
process.emit("unhandledRejection", fetchErr, Promise.resolve());
expect(exitCalls).toEqual([]);
expect(consoleWarnSpy).toHaveBeenCalledWith(
"[openclaw] Non-fatal unhandled rejection (continuing):",
expect.stringContaining("fetch failed"),
);
});
it("does NOT exit on undici fetch failures without cause", () => {
const fetchErr = new TypeError("fetch failed");
process.emit("unhandledRejection", fetchErr, Promise.resolve());
expect(exitCalls).toEqual([]);
expect(consoleWarnSpy).toHaveBeenCalledWith(
"[openclaw] Non-fatal unhandled rejection (continuing):",
expect.stringContaining("fetch failed"),
);
});
it("does NOT exit on DNS resolution failures", () => {
const dnsErr = Object.assign(new Error("DNS resolve failed"), {
code: "UND_ERR_DNS_RESOLVE_FAILED",

View File

@ -81,10 +81,10 @@ export function isTransientNetworkError(err: unknown): boolean {
const code = extractErrorCodeWithCause(err);
if (code && TRANSIENT_NETWORK_CODES.has(code)) return true;
// "fetch failed" TypeError from undici (Node's native fetch)
// "fetch failed" TypeError from undici (Node's native fetch).
// Always treat as transient — the cause may carry an unknown code
// but the outer TypeError already indicates a network-level failure.
if (err instanceof TypeError && err.message === "fetch failed") {
const cause = getErrorCause(err);
if (cause) return isTransientNetworkError(cause);
return true;
}