fix(gateway): expand transient network error detection patterns

Add more DNS error codes (EAI_NODATA, EAI_NONAME), TLS certificate
errors, HTTP status codes (429, 502, 503, 504), and message-based
fallbacks to prevent gateway crash loops on transient network issues.
This commit is contained in:
Trevin Chow 2026-01-29 12:23:02 -08:00 committed by Trevin Chow
parent 5d77b603e6
commit bdbef04ac3
2 changed files with 92 additions and 0 deletions

View File

@ -127,3 +127,63 @@ describe("isTransientNetworkError", () => {
expect(isTransientNetworkError(error)).toBe(false); expect(isTransientNetworkError(error)).toBe(false);
}); });
}); });
describe("isTransientNetworkError - expanded patterns", () => {
it("recognizes EAI_NODATA as transient", () => {
const err = { code: "EAI_NODATA", message: "DNS lookup failed" };
expect(isTransientNetworkError(err)).toBe(true);
});
it("recognizes EAI_NONAME as transient", () => {
const err = { code: "EAI_NONAME", message: "DNS name not found" };
expect(isTransientNetworkError(err)).toBe(true);
});
it("recognizes HTTP 502 as transient", () => {
const err = { status: 502, message: "Bad Gateway" };
expect(isTransientNetworkError(err)).toBe(true);
});
it("recognizes HTTP 503 as transient", () => {
const err = { statusCode: 503, message: "Service Unavailable" };
expect(isTransientNetworkError(err)).toBe(true);
});
it("recognizes HTTP 429 rate limit as transient", () => {
const err = { status: 429, message: "Too Many Requests" };
expect(isTransientNetworkError(err)).toBe(true);
});
it("recognizes socket closed message as transient", () => {
const err = new Error("socket closed unexpectedly");
expect(isTransientNetworkError(err)).toBe(true);
});
it("recognizes client network socket disconnected as transient", () => {
const err = new Error("Client network socket disconnected before secure TLS connection");
expect(isTransientNetworkError(err)).toBe(true);
});
it("recognizes TLS certificate errors as transient", () => {
const err1 = { code: "CERT_HAS_EXPIRED", message: "certificate has expired" };
expect(isTransientNetworkError(err1)).toBe(true);
const err2 = { code: "ERR_TLS_CERT_ALTNAME_INVALID", message: "Hostname mismatch" };
expect(isTransientNetworkError(err2)).toBe(true);
});
it("does not recognize HTTP 400 as transient", () => {
const err = { status: 400, message: "Bad Request" };
expect(isTransientNetworkError(err)).toBe(false);
});
it("does not recognize HTTP 401 as transient", () => {
const err = { status: 401, message: "Unauthorized" };
expect(isTransientNetworkError(err)).toBe(false);
});
it("handles string status codes", () => {
const err = { status: "503", message: "Service Unavailable" };
expect(isTransientNetworkError(err)).toBe(true);
});
});

View File

@ -28,6 +28,10 @@ const TRANSIENT_NETWORK_CODES = new Set([
"EHOSTUNREACH", "EHOSTUNREACH",
"ENETUNREACH", "ENETUNREACH",
"EAI_AGAIN", "EAI_AGAIN",
"EAI_NODATA",
"EAI_NONAME",
"CERT_HAS_EXPIRED",
"ERR_TLS_CERT_ALTNAME_INVALID",
"UND_ERR_CONNECT_TIMEOUT", "UND_ERR_CONNECT_TIMEOUT",
"UND_ERR_DNS_RESOLVE_FAILED", "UND_ERR_DNS_RESOLVE_FAILED",
"UND_ERR_CONNECT", "UND_ERR_CONNECT",
@ -99,6 +103,34 @@ export function isTransientNetworkError(err: unknown): boolean {
return err.errors.some((e) => isTransientNetworkError(e)); return err.errors.some((e) => isTransientNetworkError(e));
} }
// Message-based fallback detection
const message = err instanceof Error ? err.message?.toLowerCase() : "";
if (
message.includes("fetch failed") ||
message.includes("network error") ||
message.includes("socket hang up") ||
message.includes("socket closed") ||
message.includes("client network socket disconnected")
) {
return true;
}
// Check for transient HTTP error responses (handle both number and string status codes)
const statusRaw =
(err as { status?: unknown }).status ?? (err as { statusCode?: unknown }).statusCode;
const status =
typeof statusRaw === "number"
? statusRaw
: typeof statusRaw === "string"
? parseInt(statusRaw, 10)
: NaN;
if (
Number.isFinite(status) &&
(status === 429 || status === 502 || status === 503 || status === 504)
) {
return true;
}
return false; return false;
} }