node:http: enforce server headersTimeout and requestTimeout

JA
1mo ago
+1355-14

What

Server.headersTimeout and Server.requestTimeout (node:http/https) were validated and stored on the server object, but never enforced: the node:http server is created with idleTimeout: 0, so a connection with stalled request headers, a stalled request body, or that never sent a byte was kept open indefinitely.

This wires both values into uWS as per-socket receive deadlines:

  • From connection open (or the completion of the previous keep-alive exchange) until a message's header section is complete, the socket timer is armed with headersTimeout (default 60s).
  • While the message body is still incomplete, it is armed with requestTimeout (default 300s).
  • When such a deadline expires, the server emits 'clientError' with ERR_HTTP_REQUEST_TIMEOUT ("Request timeout"), writes HTTP/1.1 408 Request Timeout\r\nConnection: close\r\n\r\n when nothing has been written for that message yet, and closes the socket.
  • Once the message is fully received, the deadline is dropped: handlers, in-flight responses, and idle keep-alive sockets have no receive deadline. 0 disables either value.

All of it is gated on a node-only uWS context option (setNodeReceiveTimeouts), so Bun.serve idleTimeout, keep-alive, and upgrade behavior is untouched (Bun.serve sockets keep the legacy 10s pre-request timeout).

How it was verified

Behavior was derived from real Node (v26.3.0) fixtures and matched case by case: stalled first headers, zero-byte connection, stalled body after dispatch (handler runs, then 'clientError' + req 'aborted' + 408), stalled second keep-alive request, fully-received request with a slow handler (not killed), idle keep-alive socket (not reaped by these timeouts), both values 0 (disabled), and a user 'clientError' listener taking over teardown.

Known bounded deviations from Node: expiry granularity is uWS's ~4s timer sweep rather than connectionsCheckingInterval (Node's default check interval is 30s, so both are coarse); the deadline is re-armed per received chunk (inactivity-based) instead of an absolute per-message deadline; the canned 408 is written from native code even when a synchronous 'clientError' listener has not yet destroyed the socket (same as the existing native 400/431 parse-error responses); values are applied at listen() time; per-socket values above ~940s are capped (uWS timer slot range).

Tests

New cases in test/js/node/http/node-http.test.ts ("server headersTimeout/requestTimeout enforcement"): http+https stalled headers => exact 408 bytes + close; 'clientError' ERR_HTTP_REQUEST_TIMEOUT; stalled body => handler dispatched then aborted with a 408; stalled second keep-alive request; fully-received request with a slow handler still gets its 200; timeouts disabled with 0. All except the two negative-contract cases fail on the previous behavior (USE_SYSTEM_BUN=1).

Also re-ran: the full test/js/node/http/ suite, the vendored test-http(s)-server-*timeout*, keep-alive, upgrade and CONNECT node parallel tests, test/js/bun/http/serve.test.ts, and the Bun.serve websocket suites.