fix(server): raise keepAliveTimeout above ingress-nginx's upstream idle timeout
Build and Publish / Metadata (push) Successful in 1s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 1m2s

Node's default keepAliveTimeout (5s) is shorter than ingress-nginx's
default upstream keepalive idle timeout (60s). Under that mismatch,
nginx can forward a request onto a pooled connection Node has *just*
closed, which the browser sees as a 502 — and it never shows up in
this process's own traces because Node never received the request.
/api/rooms is polled every 5s from idle clients, landing almost
exactly on that 5s boundary, which is why it was the one showing up
for external users.

Also add abort/slow-request logging (tagged with the active trace ID
so it can be pivoted to from RUM) and a clientError handler, so a
genuine backend-side failure is now visible instead of indistinguishable
from the proxy-side race.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
matsandClaude Sonnet 5 committed 2026-09-22 11:21:52 +02:00
1 parent 5bde1757d6
commit 664417c6da
1 file changed
+33
+33
View File
@@ -7,6 +7,11 @@ const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { WebSocketServer } = require('ws');
const { trace } = require('@opentelemetry/api');
function traceId() {
return trace.getActiveSpan()?.spanContext().traceId;
}
const PORT = Number(process.env.PORT || 8080);
const HOST = process.env.HOST || '0.0.0.0';
@@ -92,6 +97,20 @@ function serveStatic(req, res) {
}
const server = http.createServer((req, res) => {
const start = process.hrtime.bigint();
const { method, url } = req;
res.on('close', () => {
const ms = Number(process.hrtime.bigint() - start) / 1e6;
// res.writableEnded false here means the client (or ingress) dropped the
// connection before we finished writing — the smoking gun for a 502 that
// originates on the proxy side rather than in this process.
if (!res.writableEnded) {
console.warn(`[http] ABORTED ${method} ${url} after ${ms.toFixed(1)}ms (status=${res.statusCode}) trace=${traceId()}`);
} else if (ms > 1000) {
console.warn(`[http] SLOW ${method} ${url} ${res.statusCode} ${ms.toFixed(1)}ms trace=${traceId()}`);
}
});
if (req.url === '/healthz' || req.url === '/readyz' || req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('ok');
@@ -110,6 +129,20 @@ const server = http.createServer((req, res) => {
serveStatic(req, res);
});
// Node's default keepAliveTimeout (5s) is shorter than ingress-nginx's default
// upstream keepalive idle timeout (60s). Under that mismatch, nginx can pick a
// pooled connection Node has *just* closed and forward a request onto it,
// which comes back to the browser as a 502 — invisible from inside this
// process because Node never saw the request. headersTimeout must stay above
// keepAliveTimeout (Node enforces this).
server.keepAliveTimeout = 65_000;
server.headersTimeout = 66_000;
server.on('clientError', (err, socket) => {
console.warn(`[http] clientError: ${err.message} (code=${err.code})`);
if (socket.writable) socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});
// ---------------------------------------------------------------------------
// Signaling
// client -> server: {type: offer|answer|candidate, to} {type: pos|state}