Bun.connect: make end()/shutdown() a write-side half-close; fix timeout() docs
What
Socket.end() and Socket.shutdown() on Bun.connect/Bun.listen sockets did not match their documented contract:
| Call | Documented | Actual (before) | Actual (after) |
|---|---|---|---|
end() | SHUT_WR, stay readable | close(2), nothing received | SHUT_WR, stay readable |
shutdown() | SHUT_RDWR | shutdown(SHUT_WR) | shutdown(SHUT_WR) |
shutdown(false) | SHUT_RDWR | shutdown(SHUT_WR) | shutdown(SHUT_WR) |
shutdown(true) | SHUT_WR, stay readable | shutdown(SHUT_RD), nothing received | shutdown(SHUT_WR) |
So an app that follows the docs for a request-then-read-reply protocol loses the peer's entire reply with end() or shutdown(true).
Cause
Socket.prototype.shutdownmappedarg.to_boolean()toshutdown_read()(SHUT_RD) vsshutdown()(SHUT_WR) (socket_body.rs).Socket.prototype.endsetEND_AFTER_FLUSH, andinternal_flush()turned that intomark_inactive()which callsclose_and_detach(Normal)(close(2)), not a half-close.
Fix
shutdown()ignores its argument and always issuesSHUT_WR.SHUT_RDhas no safe user-facing use (the peer cannot observe it and uSockets fabricates a localend).internal_flush()replacesmark_inactive()withsocket.shutdown()for the end-after-flush arm. The socket stays readable; when the peer's FIN arrives uSockets' existingeof && is_shut_downbranch inloop.ccloses it andon_closeruns the same teardown that used to run here.node:net's_finalalready calledshutdown()directly, so its half-close behavior is unchanged. The serveronconnectiondrop path (blockList/maxConnections) switches fromclientHandle.end()toclientHandle.close()since it wants an immediate close, matching Node'sclientHandle.close().bun.d.ts: rewrite theshutdown()andtimeout()JSDoc to describe the actual behavior; delete the self-contradictoryend(): void"Close the socket immediately" overload.timeout()is notify-only (the handler must close the socket itself) and rounds up to the 4-secondLIBUS_TIMEOUT_GRANULARITYsweep.
Verification
Fail-before (released bun):
(fail) end() sends FIN and keeps the read side open { received: 0, readyState: -1, writeAfter: -1 }
(fail) shutdown(true) sends FIN and keeps the read side { received: 0, readyState: 1, writeAfter: 1 }
(pass) shutdown() sends FIN and keeps the read side open
(pass) shutdown(false) sends FIN and keeps the read side open
Pass-after (bun bd test):
(pass) end() sends FIN and keeps the read side open
(pass) shutdown() sends FIN and keeps the read side open
(pass) shutdown(false) sends FIN and keeps the read side open
(pass) shutdown(true) sends FIN and keeps the read side open
test/js/bun/net/, test/js/node/net/, test/js/node/tls/, test/js/node/http/, test/js/node/http2/ all match main (pre-existing local failures only). test-net-server-drop-connections.js, test-net-server-max-connections.js, test-net-server-blocklist.js, test-net-server-max-connections-close-makes-more-available.js pass. bun-types integration test passes.
Supersedes #35932 (docs-only approach).
no test proof · iteration 2 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/bun/net/socket.test.ts