Files
diegosouzapw--omniroute/tests/unit/proxyfetch-retry-fresh-socket-4252.test.ts
2026-07-13 13:39:12 +08:00

80 lines
3.1 KiB
TypeScript

/**
* #4252 — Undici dispatcher fails on direct provider requests in 502 bursts.
*
* The default direct dispatcher pools keep-alive sockets for up to
* `fetchKeepAliveTimeoutMs` (4 s). Edges like nvidia / opencode-zen silently
* close idle keep-alive sockets within that window, so the next request reusing
* a pooled socket fails with `UND_ERR_SOCKET` ("other side closed") — in bursts.
*
* proxyFetch retries once on such transient socket errors, but the retry reused
* the SAME pooled dispatcher (`getDefaultDispatcher()`), so it could grab ANOTHER
* stale socket and fail too → fall through to native fetch (which also pools) →
* the job sat in the rate-limit queue until the 30 s timeout → 502 + circuit
* breaker open.
*
* The retry must use a fresh, no-keep-alive dispatcher so it opens a brand-new
* socket that cannot be a dead pooled one — converting the burst into a clean
* retry success. The first attempt still uses the pooled dispatcher (healthy
* keep-alive reuse is preserved).
*/
import { test } from "node:test";
import assert from "node:assert/strict";
import { proxyFetch } from "../../open-sse/utils/proxyFetch.ts";
import {
getDefaultDispatcher,
getRetryDispatcher,
} from "../../open-sse/utils/proxyDispatcher.ts";
function undErrSocket(): Error {
const err = new Error("fetch failed") as Error & { code?: string };
err.code = "UND_ERR_SOCKET";
return err;
}
test("#4252 a transient socket failure retries on a FRESH (no-keep-alive) dispatcher, not the pooled one", async () => {
const dispatchersUsed: unknown[] = [];
let undiciCalls = 0;
let nativeCalls = 0;
const mockUndici = async (_input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
undiciCalls++;
dispatchersUsed.push((init as { dispatcher?: unknown } | undefined)?.dispatcher);
if (undiciCalls === 1) {
// First attempt grabs a stale pooled keep-alive socket the edge already closed.
throw undErrSocket();
}
return new Response("ok", { status: 200 });
};
const mockNative = async (): Promise<Response> =>
new Response("native-should-not-fire", { status: 200 });
const res = await proxyFetch(
"https://integrate.api.nvidia.com/v1/chat/completions",
{ method: "POST" },
{ undiciFetch: mockUndici, nativeFetch: mockNative }
);
assert.equal(undiciCalls, 2, "undici must retry once (initial fail + retry)");
assert.equal(nativeCalls, 0, "native fallback must NOT fire — the retry recovers it");
assert.equal(await res.text(), "ok");
// The actual regression guard: attempt 0 uses the pooled keep-alive dispatcher,
// the retry uses the fresh no-keep-alive dispatcher (a DIFFERENT instance) so it
// can't reuse another dead pooled socket.
assert.equal(
dispatchersUsed[0],
getDefaultDispatcher(),
"first attempt must use the pooled default dispatcher"
);
assert.equal(
dispatchersUsed[1],
getRetryDispatcher(),
"retry must use the fresh no-keep-alive retry dispatcher"
);
assert.notEqual(
dispatchersUsed[0],
dispatchersUsed[1],
"retry must NOT reuse the same pooled dispatcher (would grab another stale socket)"
);
});