test(e2e): a localhost connect the machine dropped is tried again on the test side
On a Windows host a burst of test traffic can drop a loopback connect now and then (every port, any program — measured on the dev desktop). The browser resends it and only waits; Node (libuv) sends a loopback SYN once and fails at ~300 ms with "connect ETIMEDOUT". `e2e/retryConnect.ts` wraps Playwright's request context so that one error is tried again (0.5 / 1 / 2 / 4 / 8 s, the system's own retry waits) while every other failure goes straight to the test; global.setup's health check, login and /api/me and speed.measure's share probe go through it. Unit-tested; e2e 61/61.
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
// 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];
|
||||
|
||||
/** `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 async (...args: unknown[]) => {
|
||||
for (let i = 0; ; i++) {
|
||||
try {
|
||||
return await v.apply(target, args);
|
||||
} catch (e) {
|
||||
const wait = WAITS_MS[i];
|
||||
if (wait === undefined || !NO_CONNECTION.test(String((e as Error)?.message))) throw e;
|
||||
await new Promise((r) => setTimeout(r, wait));
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user