From 7b17e0582682addb4b32cbb00729c087f8b241dc Mon Sep 17 00:00:00 2001 From: "Claude Opus 4.8" Date: Wed, 8 Jul 2026 22:34:01 +0200 Subject: [PATCH] Fix e2e static server crashing on an abruptly-dropped collab WebSocket (#38) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of the CI-only Auth-e2e failure (found in the runner log): the offline pack started with `connect ECONNREFUSED :5173` — the e2e static web server had already exited. Its `/collab` WebSocket proxy attached an 'error' handler to the upstream socket but not to the client socket, so a WebSocket dropped abruptly (a context closing at the end of the collab pack, or the offline toggle) raised an unhandled 'error' → uncaught exception → the whole test server crashed, failing the next pack. It only reproduced on the CI runner (Linux/Node 22 emits 'error'; local macOS/Node 26 emitted 'close'). Handle 'error' on the client socket too, and add a last-resort `uncaughtException` guard so this throwaway test server can never be taken down mid-run by a stray socket error. Verified locally by running the collab and offline packs back-to-back against a single server process. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01PGdhRiwU1WRL4XxJfZYipY --- scripts/e2e-static-server.mjs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/scripts/e2e-static-server.mjs b/scripts/e2e-static-server.mjs index 6ea4365..45fb0b3 100644 --- a/scripts/e2e-static-server.mjs +++ b/scripts/e2e-static-server.mjs @@ -80,6 +80,11 @@ server.on('upgrade', (req, socket, head) => { socket.destroy(); return; } + // A WebSocket that is dropped abruptly (e.g. a test toggling the browser + // offline, or a context closing) emits 'error' on these sockets. Without a + // listener that becomes an uncaught exception that would crash this server — + // so always handle it and just tear the pair down. + socket.on('error', () => socket.destroy()); const upstream = httpRequest({ hostname: COLLAB_TARGET.hostname, port: COLLAB_TARGET.port, @@ -88,6 +93,7 @@ server.on('upgrade', (req, socket, head) => { headers: { ...req.headers, host: `${COLLAB_TARGET.hostname}:${COLLAB_TARGET.port}` }, }); upstream.on('upgrade', (upstreamRes, upstreamSocket, upstreamHead) => { + upstreamSocket.on('error', () => socket.destroy()); const statusLine = `HTTP/1.1 ${upstreamRes.statusCode} ${upstreamRes.statusMessage}\r\n`; const headerLines = Object.entries(upstreamRes.headers) .map(([key, value]) => `${key}: ${value}`) @@ -96,13 +102,18 @@ server.on('upgrade', (req, socket, head) => { if (upstreamHead?.length) socket.write(upstreamHead); upstreamSocket.pipe(socket); socket.pipe(upstreamSocket); - upstreamSocket.on('error', () => socket.destroy()); }); upstream.on('error', () => socket.destroy()); if (head?.length) upstream.write(head); upstream.end(); }); +// Last-resort guard: this is a throwaway test server, so a stray socket error +// must never take it down mid-run and fail an unrelated later test pack. +process.on('uncaughtException', (error) => { + console.error('e2e static server: ignored uncaught error:', error?.message ?? error); +}); + server.listen(PORT, () => console.log( `e2e static server on :${PORT} → api ${API_TARGET.href} collab ${COLLAB_TARGET.href}`,