net::ERR_NO_BUFFER_SPACE stopped a release at its first page with nothing else running, and the unchanged rerun passed. Every page load of the suite now goes through one helper, held by a test.
50 lines
2.6 KiB
TypeScript
50 lines
2.6 KiB
TypeScript
// Playwright's request context (it runs in Node), trying a connection again when it never came about —
|
||
// as every TCP stack does. Measured on the Windows dev desktop (2026-10-10, the work-group-timesheet
|
||
// suite): now and then, in bursts of 1–15 s, a connect to localhost is dropped — every port, IPv4 and
|
||
// IPv6, any program, no Docker needed; set off by any big test run on the machine. Every other client
|
||
// sends its SYN again and gets in a moment later, but Node (libuv) sends a loopback SYN only once on
|
||
// Windows and fails at ~300 ms with "connect ETIMEDOUT". The request never left this side, so trying it
|
||
// again is safe: the waits the system's own retries take (0.5, 1, 2, 4, 8 s). Anything else — a refused
|
||
// port, a reset, an answer — goes straight to the test. (The browser resends its own SYN: a page only
|
||
// waits, and nothing here can or should hide that.)
|
||
const METHODS = new Set(["get", "post", "put", "patch", "delete", "head", "fetch"]);
|
||
const NO_CONNECTION = /\bconnect ETIMEDOUT\b/;
|
||
const WAITS_MS = [500, 1000, 2000, 4000, 8000];
|
||
|
||
/** `attempt` again after each wait while it fails with `transient`; any other failure goes on. */
|
||
async function tryAgain<R>(attempt: () => Promise<R>, transient: RegExp): Promise<R> {
|
||
for (let i = 0; ; i++) {
|
||
try {
|
||
return await attempt();
|
||
} catch (e) {
|
||
const wait = WAITS_MS[i];
|
||
if (wait === undefined || !transient.test(String((e as Error)?.message))) throw e;
|
||
await new Promise((r) => setTimeout(r, wait));
|
||
}
|
||
}
|
||
}
|
||
|
||
/** `ctx` with its request methods trying a connect that timed out again (see above). */
|
||
export function retryConnect<T extends object>(ctx: T): T {
|
||
return new Proxy(ctx, {
|
||
get(target, key) {
|
||
const v: unknown = Reflect.get(target, key, target);
|
||
if (typeof v !== "function") return v;
|
||
if (typeof key !== "string" || !METHODS.has(key)) return v.bind(target);
|
||
return (...args: unknown[]) =>
|
||
tryAgain(() => Promise.resolve(v.apply(target, args)), NO_CONNECTION);
|
||
},
|
||
});
|
||
}
|
||
|
||
// A page load has a failure of its own on the same machine: `net::ERR_NO_BUFFER_SPACE`, the socket
|
||
// call refused for the moment. It stopped a release's e2e at its first page (0.116.3, with nothing
|
||
// else running; 0.107.3 aborted on it too), and the unchanged rerun passed. A browser does not try
|
||
// a main document again by itself, and the request never left, so this does. The suite's every page
|
||
// load goes through here (`e2eRetryConnect.test.ts`).
|
||
const NO_BUFFER = /\bERR_NO_BUFFER_SPACE\b/;
|
||
|
||
export function gotoRetrying<R>(page: { goto(url: string): Promise<R> }, url: string): Promise<R> {
|
||
return tryAgain(() => page.goto(url), NO_BUFFER);
|
||
}
|