Skip to content

Commit b98c8c8

Browse files
committed
fix: keep undici terminated exceptions non-fatal
1 parent ff58d23 commit b98c8c8

2 files changed

Lines changed: 18 additions & 0 deletions

File tree

src/infra/unhandled-rejections.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,10 @@ describe("isTransientUnhandledRejectionError", () => {
364364
const wrappedWsPreHandshakeClose = Object.assign(new Error("feishu reconnect failed"), {
365365
cause: wsPreHandshakeClose,
366366
});
367+
const undiciTerminated = new TypeError("terminated");
368+
const wrappedUndiciTerminated = Object.assign(new Error("model fetch failed"), {
369+
cause: undiciTerminated,
370+
});
367371
const generic = new Error("boom");
368372

369373
expect(isBenignUncaughtExceptionError(epipe)).toBe(true);
@@ -380,6 +384,10 @@ describe("isTransientUnhandledRejectionError", () => {
380384
expect(isBenignUncaughtExceptionError(new Error("ERR_HTTP2_INVALID_SESSION"))).toBe(true);
381385
expect(isBenignUncaughtExceptionError(wsPreHandshakeClose)).toBe(true);
382386
expect(isBenignUncaughtExceptionError(wrappedWsPreHandshakeClose)).toBe(true);
387+
expect(isBenignUncaughtExceptionError(undiciTerminated)).toBe(true);
388+
expect(isBenignUncaughtExceptionError(wrappedUndiciTerminated)).toBe(true);
389+
expect(isBenignUncaughtExceptionError(new Error("terminated"))).toBe(false);
390+
expect(isBenignUncaughtExceptionError(new TypeError("terminated unexpectedly"))).toBe(false);
383391
expect(
384392
isBenignUncaughtExceptionError(
385393
new Error("WebSocket error: WebSocket was closed before the connection was established"),

src/infra/unhandled-rejections.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ const TRANSIENT_NETWORK_MESSAGE_CODE_RE =
114114
const BENIGN_UNCAUGHT_EXCEPTION_NETWORK_MESSAGE_CODE_RE =
115115
/\b(ECONNREFUSED|ENETDOWN|EHOSTUNREACH|ENETUNREACH|EADDRNOTAVAIL|EAI_AGAIN|ENOTFOUND|ETIMEDOUT|UND_ERR_CONNECT_TIMEOUT|UND_ERR_DNS_RESOLVE_FAILED|UND_ERR_CONNECT|ERR_HTTP2_INVALID_SESSION)\b/i;
116116
const WS_PRE_HANDSHAKE_CLOSE_MESSAGE = "websocket was closed before the connection was established";
117+
const UNDICI_TERMINATED_TYPE_ERROR_MESSAGE = "terminated";
117118

118119
const TRANSIENT_SQLITE_MESSAGE_CODE_RE =
119120
/\b(SQLITE_BUSY|SQLITE_CANTOPEN|SQLITE_IOERR|SQLITE_LOCKED)\b/i;
@@ -422,6 +423,15 @@ export function isTransientUnhandledRejectionError(err: unknown): boolean {
422423

423424
function isBenignUncaughtNetworkException(err: unknown): boolean {
424425
for (const candidate of collectNestedUnhandledErrorCandidates(err)) {
426+
// Undici emits this bare TypeError when a response body aborts after request start.
427+
// Keep the shape exact so unrelated "terminated" errors still take the fatal path.
428+
if (
429+
candidate instanceof TypeError &&
430+
normalizeLowercaseStringOrEmpty(candidate.message) === UNDICI_TERMINATED_TYPE_ERROR_MESSAGE
431+
) {
432+
return true;
433+
}
434+
425435
const code = extractErrorCodeOrErrno(candidate);
426436
if (code && BENIGN_UNCAUGHT_EXCEPTION_NETWORK_CODES.has(code)) {
427437
return true;

0 commit comments

Comments
 (0)