Fix e2e static server crashing on an abruptly-dropped collab WebSocket (#38)
All checks were successful
CD / Build and push images (push) Successful in 56s
CI / Lint, typecheck, test (push) Successful in 1m56s
CI / Auth e2e pack (push) Successful in 2m25s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 8s
CD / Smoke tests against Test (push) Successful in 1m12s
CD / Promote to Int (push) Successful in 11s

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGdhRiwU1WRL4XxJfZYipY
This commit is contained in:
Claude Opus 4.8 2026-07-08 22:34:01 +02:00
parent 41b259fae5
commit 7b17e05826

View File

@ -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}`,