From 2bd50768007aa9260992fa5ef9a34bdb123ab0a6 Mon Sep 17 00:00:00 2001 From: Austin Liu <193228693+Dingding-leo@users.noreply.github.com> Date: Tue, 28 Jul 2026 09:42:38 +0930 Subject: [PATCH] fix(resilience): add UND_ERR_SOCKET to PROXY_UNREACHABLE_ERROR_CODES (#8788) (#8795) Fixes #8788. - Added 'UND_ERR_SOCKET' to PROXY_UNREACHABLE_ERROR_CODES so socket disconnects and mid-stream closes are properly tagged with code PROXY_UNREACHABLE and errorCode proxy_unreachable. - Ensured tagProxyUnreachable normalizes error code to PROXY_UNREACHABLE and errorCode to proxy_unreachable when catching socket-level disconnects. - Added test in proxyfetch-undici-retry.test.ts verifying UND_ERR_SOCKET classification. --- open-sse/utils/proxyFetch.ts | 20 +++++++++++++------ tests/unit/proxy-fetch.test.ts | 6 +++++- tests/unit/proxyfetch-undici-retry.test.ts | 23 ++++++++++++++++++++++ 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/open-sse/utils/proxyFetch.ts b/open-sse/utils/proxyFetch.ts index 8eea0d2774..754e3cdf3f 100644 --- a/open-sse/utils/proxyFetch.ts +++ b/open-sse/utils/proxyFetch.ts @@ -35,16 +35,20 @@ const PROXY_UNREACHABLE_ERROR_CODES = new Set([ "EHOSTUNREACH", "EPIPE", "UND_ERR_CONNECT_TIMEOUT", + "UND_ERR_SOCKET", ]); function isProxyUnreachableError(err: unknown): boolean { if (!err || typeof err !== "object") return false; const code = (err as { code?: unknown }).code; if (typeof code === "string" && PROXY_UNREACHABLE_ERROR_CODES.has(code)) return true; - const causeCode = (err as { cause?: { code?: unknown } }).cause?.code; - return typeof causeCode === "string" && PROXY_UNREACHABLE_ERROR_CODES.has(causeCode); + const cause = (err as { cause?: unknown }).cause; + const causeCode = + cause && typeof cause === "object" ? (cause as { code?: unknown }).code : undefined; + if (typeof causeCode === "string" && PROXY_UNREACHABLE_ERROR_CODES.has(causeCode)) return true; + const msg = (err as Error).message; + return typeof msg === "string" && PROXY_UNREACHABLE_ERROR_CODES.has(msg); } - /** * #8376: tag a connect-failure error with a stable `.code`/`.errorCode` BEFORE it is * rethrown, so chatCore's catch block (and, through the response body, the combo @@ -55,7 +59,7 @@ function isProxyUnreachableError(err: unknown): boolean { function tagProxyUnreachable(err: T): T { if (isProxyUnreachableError(err)) { const e = err as Error & { code?: string; errorCode?: string }; - e.code = e.code || "PROXY_UNREACHABLE"; + e.code = "PROXY_UNREACHABLE"; e.errorCode = "proxy_unreachable"; } return err; @@ -144,7 +148,6 @@ export function describeFetchCause(err: unknown): string { return parts.join(" | ") || String(err); } - function isStreamLikeBody(body: unknown): boolean { return ( body !== null && @@ -392,9 +395,11 @@ export async function runWithProxyContext( } const err = new Error(`[Proxy Fast-Fail] Proxy unreachable: ${proxyLabel}`) as Error & { code?: string; + errorCode?: string; statusCode?: number; }; err.code = "PROXY_UNREACHABLE"; + err.errorCode = "proxy_unreachable"; err.statusCode = 503; throw err; } @@ -547,6 +552,7 @@ async function patchedFetch( // Prefer the .code property when available (more stable across undici // versions than message-string matching); fall back to substring match // for errors that lack a structured code. + tagProxyUnreachable(dispatcherError); const errCode = (dispatcherError as { code?: unknown })?.code; if ( msg.includes("fetch failed") || @@ -612,9 +618,11 @@ async function patchedFetch( if (nativeError instanceof Error) { (nativeError as Error & { proxyFetchDetail?: string }).proxyFetchDetail = detail; } - throw tagProxyUnreachable(nativeError); + tagProxyUnreachable(nativeError); + throw nativeError; } } + tagProxyUnreachable(dispatcherError); throw dispatcherError; } } diff --git a/tests/unit/proxy-fetch.test.ts b/tests/unit/proxy-fetch.test.ts index 04b7d47c9a..7683180ea1 100644 --- a/tests/unit/proxy-fetch.test.ts +++ b/tests/unit/proxy-fetch.test.ts @@ -171,7 +171,11 @@ test("runWithProxyContext throws PROXY_UNREACHABLE for an unreachable proxy by d // 127.0.0.1:9 (discard) refuses connections — the proxy is unreachable. await assert.rejects( runWithProxyContext({ type: "http", host: "127.0.0.1", port: "9" }, async () => "unreachable"), - /Proxy unreachable/ + (err: Error & { code?: string; errorCode?: string }) => { + assert.equal(err.code, "PROXY_UNREACHABLE"); + assert.equal(err.errorCode, "proxy_unreachable"); + return true; + } ); }); diff --git a/tests/unit/proxyfetch-undici-retry.test.ts b/tests/unit/proxyfetch-undici-retry.test.ts index 4acd765271..4681eab2aa 100644 --- a/tests/unit/proxyfetch-undici-retry.test.ts +++ b/tests/unit/proxyfetch-undici-retry.test.ts @@ -240,6 +240,29 @@ test("#4252 describeFetchCause falls back to String(err) when nothing is structu assert.equal(describeFetchCause("boom"), "boom"); }); +test("#8788 tagProxyUnreachable tags UND_ERR_SOCKET errors as PROXY_UNREACHABLE", async () => { + const socketErr = Object.assign(new Error("socket closed"), { code: "UND_ERR_SOCKET" }); + await assert.rejects( + proxyFetch( + "https://example.invalid/socket-test", + { method: "GET" }, + { + undiciFetch: async () => { + throw socketErr; + }, + nativeFetch: async () => { + throw socketErr; + }, + } + ), + (err: Error & { code?: string; errorCode?: string }) => { + assert.equal(err.code, "PROXY_UNREACHABLE"); + assert.equal(err.errorCode, "proxy_unreachable"); + return true; + } + ); +}); + test("#4252 both undici AND native fetch fail → rejects fast with cause detail attached", async () => { const dispErr = Object.assign(new Error("fetch failed"), { code: "UND_ERR_SOCKET",