Files
siftlode/frontend/e2e/retryConnect.ts
T
peter f2de8f73aa test(e2e): a page load the machine had no buffer for is tried again
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.
2026-11-04 00:26:50 +01:00

50 lines
2.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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);
}