Skip to content

node:http: make setKeepAlive() and the keepAlive option act on server sockets - #45006

Open
robobun wants to merge 2 commits into
mainfrom
robobun/1b34026e/http-server-socket-options
Open

robobun wants to merge 2 commits into
mainfrom
robobun/1b34026e/http-server-socket-options

Conversation

@robobun

@robobun robobun commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • socket.setKeepAlive() does nothing on the sockets of a node:http server, and createServer({ keepAlive }) is ignored. SO_KEEPALIVE stays 0, so a peer that vanishes under an open response or a tunnel is never closed.
  • The cause is the stub setKeepAlive() of NodeHTTPServerSocket (src/js/node/_http_server.ts:2145 on main). The server never reads the option.

Fix

  • The native handle of a connection gets setKeepAlive(). The stub is gone: net.Socket#setKeepAlive() finds that handle through a symbol getter and keeps its dedup.
  • http.Server keeps keepAlive and keepAliveInitialDelay as data properties. onServerConnection reads them for each connection, as Node's net.Server does at accept.
  • Verified: test/js/node/http/node-http-server-keepalive.test.ts (35 of 46 tests fail on main). Also the node:http, ws and node:net suites in Notes.
  • Self-reviewed twice: 46 concerns raised, 43 addressed. Notes names the 3 that are not.

Background

  • With TCP keep-alive the kernel probes an idle peer and closes the socket when no answer comes.
  • A server socket is a net.Socket subclass whose I/O goes through a private native handle, not _handle.
  • Considered a uSockets listener field that sets keep-alive at accept (built, measured). Its one gain is keep-alive during a TLS handshake, which the 10 s request timer already bounds.
  • Supersedes node:http server: wire SO_KEEPALIVE (createServer option + socket.setKeepAlive) #35500, which a stale sweep closed.

Downsides

  • createServer({ keepAliveInitialDelay: "5" }) now throws ERR_INVALID_ARG_TYPE, as in Node.
  • Per accepted connection: +5 bytecode ops. With keepAlive: true, +1 host call. Binary: +2,147 B text.
  • Still unlike Node: on https, keep-alive starts when the TLS handshake completes. noDelay: false and setNoDelay(false) have no effect.
Notes

How it was found. A comparison of kernel state with Node (getsockopt(2) on the accepted fd). No user report exists for it.

Dead peer. Linux, root, inside unshare -n. The client reads the first bytes, then ip link set lo down drops every packet. Seconds until the server socket closes:

Scenario Node v26.3.0 main this PR
open response, keepAlive: true, keepAliveInitialDelay: 1000 11.2 not closed in 30 11.3
open response, req.socket.setKeepAlive(true, 1000) 11.3 not closed in 30 11.3
CONNECT tunnel, keepAlive: true, keepAliveInitialDelay: 1000 11.3 not closed in 30 11.3

The time is the delay plus 10 probes of 1 s. A caller that gives no delay keeps the idle time of the kernel (7200 s on stock Linux).

Callers. @nestjs/core calls req.socket.setKeepAlive(true) for each SSE stream (packages/core/router/sse-stream.ts:122). http-proxy calls socket.setKeepAlive(true, 0) on the upgrade socket (lib/http-proxy/common.js:129, from passes/ws-incoming.js:98).

The two commits. The first makes socket.setKeepAlive() act and can land alone. The second adds the server option.

Unix sockets and Proxy. The handle of a unix socket gets no setKeepAlive(), as Node's Pipe handle has none, so the socket class has no new field. setKeepAlive() also acts through a Proxy of the socket, as in Node: the getter reads no private field. resetAndDestroy() is not changed.

Why the option is read in JS. Node's http.Server has no keep-alive code. It gives the options to net.Server, which reads two data properties in its accept callback (lib/net.js#L2361-L2365 at v26.3.0). onServerConnection is that callback here, and it already applies maxConnections and blockList the same way. An earlier shape of this PR set the option in the uSockets accept loop through a new listener field. Against Node it differed in three ways (accessor properties, a class field, and Object.defineProperty() after listen()), and it needed listener state that bun --hot carries to the next server. The JS read differs in one way: on https it runs when the handshake completes. A handshake that never completes is closed after 12 s on main and on this branch, with or without keep-alive, by the timer for a connection that has no request yet. If that timer goes away, the listener field is the way to set keep-alive earlier.

Measurements. Release builds, linux x64, main ff8ad61 against this branch, Node v26.3.0. valgrind, perf, strace and bloaty are not installed here, so the counts come from an LD_PRELOAD shim for setsockopt, gdb breakpoints, nm, size and BUN_JSC_dumpGeneratedBytecodes.

  • setsockopt sequence, 23 scenarios: 14 rows equal to Node (main: 6). Of the 13 keep-alive rows 11 are equal (main: 3). In the other 2, setKeepAlive(true, -5000) and setKeepAlive(true, 1e12), Node issues one more TCP_KEEPIDLE that the kernel rejects. The kernel state is the same.
  • Kernel state, 30 scenarios over 'request', 'connect' and 'upgrade': 21 rows equal to Node (main: 6). All 18 keep-alive rows are equal (main: 3). The 9 other rows are the noDelay rows.
  • Values assigned to server.keepAliveInitialDelay, 16 rows: 15 equal to Node. The other is true (Node sets 1 s, this PR keeps the time of the kernel).
  • Host calls per accepted connection, 1000 connections under gdb: default server 0. Server with keepAlive: true, http or https: 1 (the handle's setKeepAlive()).
  • Bytecode per accepted connection on a default server: +5 executed ops (2 in onServerConnection, 3 in the socket constructor). No allocation. req.socket: 265 B shallow and 32 own symbols, before and after. http.Server: +2 own properties.
  • net.Socket#setKeepAlive(): +2 executed ops per call on a connected socket.
  • Binary: +2,147 B text (106,483,166 to 106,485,313), +0 B data. One new host function, 694 B.
  • The new test file: main release 35 fail and 11 pass. This branch release 46 pass, 0 failures in 40 runs. This branch debug with ASAN 46 pass.

Behaviour changes for the release notes.

  • keepAlive and keepAliveInitialDelay of http.createServer() and https.createServer() act.
  • socket.setKeepAlive() acts on a server socket.
  • A keepAliveInitialDelay that is not a number throws ERR_INVALID_ARG_TYPE.
  • server.keepAlive and server.keepAliveInitialDelay exist (the delay in seconds, as in Node).

Differences from Node that stay.

  • https: keep-alive starts when the TLS handshake completes (see above). docs/runtime/nodejs-compat.mdx says so.
  • https: the socket of 'connection' is the object that req.socket is. In Node it is the raw net.Socket. So setKeepAlive(false) in a 'connection' listener of an https server with keepAlive: true does nothing here, as it does on Node's TLSSocket.
  • net.Socket compares the delay in milliseconds, Node in seconds. setKeepAlive(true, 30500) after setKeepAlive(true, 30000) sets the same 30 s again.

Not in this PR.

Suites run on the debug build of this branch. node-http.test.ts (292 pass), node-http-connect, node-http-with-ws, node-http-upgrade-body, node-https-server, node-http-maxHeadersCount, node-http-server-timeouts, node-http-incoming-socket, node-http-req-socket-pause, node-http-server-socket-end-drain, node-http-server-close-drain, node-http-server-abort-events, node-http-agent-free-socket, first_party/ws (3 files), node-net-server, node-net-allowHalfOpen, the --hot test of serve.test.ts, and 21 upstream test/js/node/test/parallel files for keep-alive, nodelay and server options.

Tests. The tests that need no socket option are in node-http-server-keepalive.node.mts, which uses node:test. The test file imports it, so they run under Bun, and its last test runs them under Node. 11 one-line mutants of the JavaScript and 2 of the C++ function each fail at least one test. The two calls to the helper from the lazy construction sites have no test: no connection of a listening server reaches them.

Self-review. Two passes. The first (28 concerns) took Nagle's algorithm, the type of service and a change to us_socket_keepalive out of an earlier draft. The second (18 concerns) replaced the uSockets listener field with the read in onServerConnection. Not addressed:

  • setKeepAlive() through a tls.TLSSocket wrap (above). It is a node:tls change.
  • One reviewer wanted to keep the listener field, because open PRs would remove the request timer. The paragraph above gives the reason against it for now.
  • A shared socket-option reader in internal-for-testing for this test file and socket.test.ts. It is a follow-up.

Review on this PR. Four optional findings, all taken:

  • resetAndDestroy() through a Proxy half-destroyed the socket after an earlier revision. That revision is gone: the method is main's again.
  • The accept path no longer calls net.Socket.prototype.setKeepAlive. It writes the two state symbols and calls the handle, as net.Server does in net.ts.
  • The tests find the fd of a connection in /proc/self/fd or /dev/fd, not in the first 1024 numbers.
  • The Bun-only tests use bun:test and static imports.

The comment check asked for five two-line comments to go. Each is one line now.

A later finding asked for the unix marker to be a class field with a default. As a field it cost 29 B for each socket (265 B to 294 B shallow), so the marker is gone: see the paragraph on unix sockets.

Not run here. Windows and macOS: CI runs them. The tests that read kernel state skip on Windows, where nothing gives the SOCKET of a server connection to JavaScript. The other tests of the file run there.


no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/node/http/node-http-server-keepalive.test.ts

@robobun

robobun commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 7:39 AM PT - Oct 17th, 2026

✅ @robobun, your commit b0794344183dae338453071a784dbe9ef06ddf95 passed in Build #127400! 🎉


🧪   To try this PR locally:

bunx bun-pr 45006

That installs a local version of the PR into your bun-45006 executable, so you can run:

bun-45006 --bun

@robobun

robobun commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status

Reproduced on main from the kernel state of the accepted connection, read with getsockopt(2) through bun:ffi. After req.socket.setKeepAlive(true, 5000), and on a server made with createServer({ keepAlive: true, keepAliveInitialDelay: 30000 }), SO_KEEPALIVE is 0. Node v26.3.0 gives 1, with TCP_KEEPIDLE 5 and 30. The reproduction is test/js/node/http/node-http-server-keepalive.test.ts: 35 of its 46 tests fail on main.

PR: #45006

@coderabbitai

coderabbitai Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: oven-sh/bun/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Essentials
  • Run ID: 39b61a33-29b0-44d8-84ca-2dedf31f59c8
📥 Commits

Reviewing files that changed from the base of the PR and between 7c96a12 and c8cedf4.

📒 Files selected for processing (6)
  • docs/runtime/nodejs-compat.mdx
  • src/js/internal/net/symbols.ts
  • src/js/node/_http_server.ts
  • src/js/node/net.ts
  • src/jsc/bindings/node/JSNodeHTTPServerSocketPrototype.cpp
  • test/js/node/http/node-http-server-keepalive.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 4 remain after this review.

Walkthrough

HTTP servers now accept keep-alive options and apply them to eligible connections. Socket keep-alive operations use native handles. Socket transport types also govern resetAndDestroy() behavior. Tests and runtime compatibility notes cover the changes.

Changes

HTTP server keep-alive

Layer / File(s) Summary
Server options and accepted connections
src/js/node/_http_server.ts
The server adds keepAlive and keepAliveInitialDelay properties. It validates and converts the delay, then applies configured settings to newly accepted connections.
Socket handle operations
src/js/internal/net/symbols.ts, src/js/node/net.ts, src/js/node/_http_server.ts, src/jsc/bindings/node/JSNodeHTTPServerSocketPrototype.cpp
Sockets expose a native handle for setKeepAlive() calls. The native method applies keep-alive settings. TCP, TLS, and Unix transport types also determine whether resetAndDestroy() is permitted.
Behavior tests and compatibility notes
test/js/node/http/node-http-server-keepalive.test.ts, docs/runtime/nodejs-compat.mdx
Tests cover server options, socket behavior, and Node.js compatibility. Runtime notes describe TCP_NODELAY behavior and when HTTPS keep-alive takes effect.

Suggested reviewers: jarred-sumner, cirospaciari

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to c8ced

No specific issue remains that calls for a change before merging; normal platform checks remain appropriate.

Pre-merge checks: Passed 4
Check name Status Explanation
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Title check Passed The title clearly and concisely describes the main change: enabling setKeepAlive() and the keepAlive option on node:http server sockets.
Description check Passed The description explains the problem, implementation, scope limits, behavior differences, and verification results. It does not use the template headings exactly, but it provides the required informat…
Full details: Description check

Explanation

The description explains the problem, implementation, scope limits, behavior differences, and verification results. It does not use the template headings exactly, but it provides the required information and is substantially complete.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline findings, I also checked the new setKeepAlive host function in JSNodeHTTPServerSocketPrototype.cpp for the usual coercion hazards: toNumber is followed by RETURN_IF_EXCEPTION and this is re-downcast and checked for isClosed() after coercion, so a valueOf that destroys the socket cannot reach us_socket_keepalive with a stale pointer, and the ms-to-seconds conversion handles NaN, negatives and values above UINT_MAX before the cast. The net.Socket.prototype.setKeepAlive fallback to kSocketOptionHandle keeps the existing dedupe state and the _handle-less early-return path unchanged for ordinary net sockets.

Extended reasoning...

The change wires TCP keep-alive through to node:http/https server sockets across JS (_http_server.ts, net.ts, symbols.ts), one new C++ host function, docs, and a new ~800-line test file; it touches no auth, crypto or injection surface, only socket options. Inline findings already flag the Proxy/resetAndDestroy regression, the user-overridable prototype call on the plain-TCP path, and test-convention issues, so a human look is already signalled; this note only records the C++ coercion and net.ts fallback checks that were examined and found sound.

Comment thread src/js/node/_http_server.ts Resolved
Comment thread src/js/node/_http_server.ts Resolved
@robobun
robobun force-pushed the robobun/1b34026e/http-server-socket-options branch from c8cedf4 to b44a8fe Compare October 16, 2026 10:32
Comment thread src/js/internal/net/symbols.ts Resolved
Comment thread src/js/internal/net/symbols.ts Resolved
Comment thread src/js/node/_http_server.ts Resolved
Comment thread src/js/node/_http_server.ts Resolved
@robobun
robobun force-pushed the robobun/1b34026e/http-server-socket-options branch from b44a8fe to 939c80b Compare October 16, 2026 10:38

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Thanks for the follow-up push: the four points from the earlier round (direct handle call instead of the public prototype method, bun:test with static imports, /proc/self/fd enumeration in fdOf, and the narrowed Proxy comment) are all addressed in the new commits. Beyond the inline nit, I also checked the new native setKeepAlive host function: the extern declaration matches us_socket_keepalive in packages/bun-usockets/src/libusockets.h, toBoolean cannot enter JS so the single RETURN_IF_EXCEPTION after toNumber covers both coercions and the liveness re-check after it is sound, and the NaN/negative/overflow handling happens in double before the unsigned cast.

Extended reasoning...

The PR adds a native setKeepAlive on the node:http server socket handle, routes net.Socket#setKeepAlive to it via a symbol getter, and makes http.Server read keepAlive/keepAliveInitialDelay per accepted connection, plus a 600-line kernel-state test suite using bun:ffi getsockopt. No security-sensitive surface beyond a new host function whose argument coercion and liveness checks I verified. The remaining inline finding is a style nit, but the change spans C++ bindings and Node-compat semantics with a deliberate design choice (JS-side accept hook vs uSockets listener field), so a maintainer should still weigh it.

Comment thread src/js/node/_http_server.ts Resolved
setKeepAlive() of a node:http server socket (req.socket, and the socket of
'connection', 'connect' and 'upgrade') was a stub that returned the socket.

The native handle of the connection now has setKeepAlive(enable, ms). The
socket class no longer overrides the method: net.Socket#setKeepAlive() finds
the handle through an internal getter, so the call that changes nothing is
skipped as for any net.Socket. A socket of a unix listener gives no handle:
it has no keep-alive, as in Node.js.
…t accepts

http.Server keeps keepAlive and keepAliveInitialDelay as own data
properties, with the check of Node's net.Server. The accept callback
reads them for each connection and sets keep-alive on the native handle,
as net.Server's onconnection does. A plain http socket starts its
setKeepAlive() state from them. An https socket starts with no state, as
Node's TLSSocket.
@robobun
robobun force-pushed the robobun/1b34026e/http-server-socket-options branch from 939c80b to b079434 Compare October 17, 2026 07:55

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed the two new commits and found no bugs; the points raised on the earlier revision (server option routed through the public prototype method, conditional kNoKeepAlive field, the Proxy claim on resetAndDestroy(), the 0..1023 fd scan, and the node:test/dynamic-import test shape) are all addressed in this version. Because it adds a native host function and changes the shared net.Socket.prototype.setKeepAlive path, a human look is still worthwhile.

What was reviewed:

  • jsFunctionNodeHTTPServerSocketSetKeepAlive: toNumber is followed by RETURN_IF_EXCEPTION and this is re-downcast and isClosed()-checked after the coercion; NaN/negative/Infinity are clamped in double before the cast to unsigned, and us_socket_keepalive itself skips a shut-down socket.
  • All three NodeHTTPServerSocket creation sites (kRealListen lazy path, onServerConnection, onServerClientError) call setKeepAliveOfServer; the unix-listener case clears handle.setKeepAlive so the net.ts guard early-returns; net.Socket#setKeepAlive only consults kSocketOptionHandle when _handle is absent, so plain net.Sockets are unaffected.
  • Option parsing matches Node's net.Server (validateNumber, clamp <0, ~~(ms/1000) stored in seconds); the ms-vs-seconds conversion back to the handle is consistent with the kSetKeepAliveInitialDelay cache that net.ts compares in ms.
  • Tests use bun:test with static imports, port: 0, resolve fds via /proc/self/fd or /dev/fd, drain the Node subprocess pipes concurrently, and skip the kernel-option cases on Windows.
Extended reasoning...

The change adds a DontEnum setKeepAlive host function on the node:http server socket handle (src/jsc/bindings/node/JSNodeHTTPServerSocketPrototype.cpp), moves two keep-alive symbols into internal/net/symbols.ts, lets net.Socket.prototype.setKeepAlive fall back to a symbol-keyed handle getter, and makes http.Server parse and apply keepAlive/keepAliveInitialDelay at every socket-creation site, plus a docs line and ~830 lines of tests. It touches no auth, crypto, or injection surface; the kernel option is set via the existing us_socket_keepalive helper. No CODEOWNERS entry covers the changed paths and the earlier inline nits are addressed in the code. Deferred rather than approved because it introduces a new native binding and alters a shared net.Socket prototype method used by every socket type, which is beyond a mechanical change.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant