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.
This commit is contained in:
Austin Liu
2026-07-28 09:42:38 +09:30
committed by GitHub
parent aecf2fccef
commit 2bd5076800
3 changed files with 42 additions and 7 deletions

View File

@@ -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<T>(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;
}
}

View File

@@ -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;
}
);
});

View File

@@ -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",