node: tls/https v26 compat wave 2 — allowHalfOpen close_notify, fetch setDefaultCACertificates, https.Server setSecureContext/keylog/TLSSocket (+6 tests) - #35535
Draft
cirospaciari wants to merge 7 commits into
Conversation
…ertificates to fetch - uSockets: extend the TLS EOF dispatch (peer close_notify / raw FIN) to Bun.connect/Bun.listen TLS sockets so allow_half_open keeps the write side alive like the plain-TCP path in loop.c; the on_writable SENT_SHUTDOWN suppression stays scoped to uWS HTTP sockets - bridge tls.setDefaultCACertificates() to the HTTP client thread: the normalized PEM set is published process-wide and the default HTTPS SSL_CTX is rebuilt on the next connect (empty set = empty trust store) - fetch: certificate failures now carry a .cause error with the same code/message, matching the err.cause.code contract of Node's fetch Vendored passing upstream tests: test-tls-connect-allow-half-open-option.js test-tls-set-default-ca-certificates-append-fetch.mjs test-tls-set-default-ca-certificates-reset-fetch.mjs
…S EOF dispatch on completed handshake
- https.Server#setSecureContext(): new us_listen_socket_set_default_ssl_ctx
swaps the listener's default SSL_CTX (what accepted sockets SSL_new from),
re-registering the listener-level SNI callbacks on the new context;
established connections keep the old context through their own SSL refs.
The JS method reuses the constructor's TLS-option processing (extracted to
processServerTlsOptions) and is installed on TLS servers only
- encrypted server sockets are built with the NodeHTTPServerSocket method
table spliced above TLSSocket.prototype (Reflect.construct with a prepared
new.target), so socket instanceof tls.TLSSocket holds like Node without
running TLSSocket's constructor
- server 'keylog': the first listener arms per-listener key-log parking
(checked at accept in us_internal_ssl_attach); parked lines stay queued
for uWS sockets (no keylog dispatch) and are drained into
server.emit('keylog', line, socket) by the connection callback
- the TLS EOF dispatch added for allowHalfOpen now requires a COMPLETED
handshake for Bun sockets: an EOF mid-handshake keeps the historical
force-close so it surfaces as ECONNRESET 'socket hang up'
(test-tls-econnreset, test-tls-sni-servername regressed without this)
Vendored passing upstream tests:
test-tls-set-secure-context.js
test-https-timeout-server-2.js
test-https-agent-keylog.js
…icing TLSSocket.prototype - JSNodeHTTPServerSocket::close() (the socket.destroy() path) closed with CLEAN_SHUTDOWN, which the TLS layer treats as graceful: it defers the fd close until the peer's close_notify reply. An allowHalfOpen peer never sends one, so the destroyed server socket pinned the event loop forever (test-https-set-timeout-server's idleTimeout hung). destroy is forceful: close with FAST_SHUTDOWN - the spliced TLS server-socket prototype now pins the previous resolution for any name TLSSocket.prototype owns that the NetSocket chain also provided, so only genuinely TLS-only surface resolves through TLSSocket.prototype
Collaborator
|
Updated 12:20 AM PT - Aug 4th, 2026
❌ @robobun, your commit 9b53c58 has 1 failures in
🧪 To try this PR locally: bunx bun-pr 35535That installs a local version of the PR into your bun-35535 --bun |
Contributor
|
Found 4 issues this PR may fix:
🤖 Generated with Claude Code |
…e/node-tls-v26-wave2 # Conflicts: # packages/bun-usockets/src/crypto/openssl.c # packages/bun-usockets/src/libusockets.h # src/runtime/server/server_body.rs # src/runtime/webcore/fetch/FetchTasklet.rs
…e/node-tls-v26-wave2
Collaborator
|
Cross-reference: the
|
This was referenced Aug 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #35386 (base branch
claude/node-tls-v26b), which stacks on #34598 — review those first. Second TLS wave: the classes the first wave root-caused but could not fix because they live in code the #34598 rewrite owns. +6 upstream Node v26.3.0 tests vendored byte-verbatim.TLS half-open: honor
allowHalfOpenon the peer's close_notify (+1)packages/bun-usockets/src/crypto/openssl.cscoped the TLS EOF dispatch (peer close_notify →SSL_ERROR_ZERO_RETURN, and the raw-FIN path inus_internal_ssl_on_end) to uWS HTTP server sockets; every other TLS socket kind force-closed the transport. Atls.connect({ allowHalfOpen: true })client therefore lost its write side the moment the server half-closed — writing after'end'failed instead of flushing.The fixing line extends
ssl_wants_eof_dispatchtoBUN_SOCKET_KIND_BUN_SOCKET_TLS(Bun.connect/Bun.listen sockets, which node:tls rides on) for established sessions only, making the TLS EOF path mirror the plain-TCPallow_half_openbranch inloop.c. An EOF mid-handshake keeps the historical force-close so it still surfaces as ECONNRESET "socket hang up" (test-tls-econnresetandtest-tls-sni-servernameregressed without the handshake gate; both re-verified 3x). TheSENT_SHUTDOWNwritable-dispatch suppression stays scoped to uWS HTTP sockets. HTTP client / WebSocket / DB driver sockets keep the historical force-close.test-tls-connect-allow-half-open-option.jsfetch() honors
tls.setDefaultCACertificates()(+2)The override only updated a JS-side store consumed by
node:tls/node:https;fetch()'s TLS contexts live on the HTTP client thread and never saw it. The normalized PEM set is now published process-wide (src/http/default_ca.rs, mutex + generation counter) and the HTTP thread rebuilds the default HTTPSSSL_CTXon the next connect when the generation moved (ensure_https_context_init→HTTPContext::replace_ssl_ctx_with_default_ca). An empty array installs an explicitly empty trust store, like Node. The freshSSL_CTXstarts with an empty client session cache, so resumption cannot skip re-verification against the new set.fetch's certificate-failure rejection now also carries
.cause— a distinct error instance with the same code/message — matching theerr.cause.codecontract of Node's fetch. The top-level error shape (code/message/path) is unchanged.Deliberate scope, matching Node/undici: keep-alive pooled connections established under the previous CA set are not flushed (the upstream test dodges its own session cache by switching hosts).
test-tls-set-default-ca-certificates-append-fetch.mjs,test-tls-set-default-ca-certificates-reset-fetch.mjshttps.Server surface (+3)
setSecureContext(options)— newus_listen_socket_set_default_ssl_ctxswaps the listen socket's defaultSSL_CTX(the one accepted socketsSSL_newfrom), re-registering the listener-level SNI/select-cert callbacks on the new context. In-flight and established connections keep the old context through their ownSSLrefs, so an active response streams across the swap (the vendored test asserts exactly this). The JS method reuses the constructor's TLS-option processing (extracted toprocessServerTlsOptions, no behavior change) and is installed per-instance on TLS servers only, so plainhttp.Serverkeeps Node's surface.socket instanceof tls.TLSSocket— encrypted server sockets are constructed with a prototype chain that splices the NodeHTTPServerSocket method table aboveTLSSocket.prototype(Reflect.constructwith a prepared new.target). TLSSocket's constructor never runs — the NodeHTTP handle replaces its wrap machinery — and any nameTLSSocket.prototypeowns that the old NetSocket chain also provided keeps its previous resolution, so only genuinely TLS-only surface (getPeerCertificate, …) resolves throughTLSSocket.prototype.server
'keylog'— the first'keylog'listener arms key-log parking on the listener (us_listen_socket_enable_keylog, checked at accept inus_internal_ssl_attach); the handshake's NSS lines are parked on each accepted socket'sSSL(the same queue the node:tls client path uses) and drained intoserver.emit('keylog', line, socket)when the handshake-complete connection callback fires. Zero cost when no listener exists. Post-handshake lines (TLS 1.3 KeyUpdate) are not surfaced; commented inline.socket.destroy()on a server socket now force-closes —JSNodeHTTPServerSocket::close()closed withCLEAN_SHUTDOWN(0), which the TLS layer treats as graceful and defers the fd close until the peer's close_notify reply. AnallowHalfOpenpeer never sends one, so a destroyed server socket pinned the event loop forever — previously masked because such clients were themselves force-closed; exposed by the half-open fix above (test-https-set-timeout-server's idleTimeout hung). destroy is forceful:FAST_SHUTDOWN.vendored:
test-tls-set-secure-context.js,test-https-timeout-server-2.js,test-https-agent-keylog.jsNot fixed here (investigated; blocking mechanism identified)
test-https-resume-after-renew.js,test-tls-ticket-12.js— needserver._sharedCreds.context.enableTicketKeyCallback()/ server-side session-ticket key control: a full ticket-key callback bridge into BoringSSL'sSSL_CTX_set_tlsext_ticket_key_cbplus the_sharedCredsobject graph on the Bun.serve-backed server.test-double-tls-client.js— TLS-over-TLS (tls.connect({ socket: tlsSocket })) fails during the inner handshake on the duplex-backed TLS path ("disconnected before secure TLS connection was established").SSL_trace, PSK, internalBinding(tls_wrap/stream_wrap)-dependent files, per-record'data'framing (max-send-fragment),--tls-cipher-list,NODE_DEBUG_NATIVElog-line asserts.Verification
BUN_GARBAGE_COLLECTOR_LEVEL=1 BUN_FEATURE_FLAG_INTERNAL_FOR_TESTING=1 --config=bunfig.node-test.toml, one process per file): 3 consecutive green rounds on the final build, and each fails on the unfixed base build (allowHalfOpen: write-after-end error; CA fetch:UNABLE_TO_VERIFY_LEAF_SIGNATUREreject missing/.causeabsent; setSecureContext:not a function; timeout-server-2:instanceofassert; keylog: 0 of 10 lines).test-tls-*/test-https-*upstream files re-run one process per file against the fix; the two same-class regressions the first run caught (econnreset, sni-servername) are fixed by the handshake gate and re-verified; the fulltest-https-*subset (76 files) re-run green on the final build.bun bd test test/js/node/tls/243 pass,test/js/node/net/220 pass,test/js/web/fetch/fetch.tls.test.ts23 pass;test/js/node/http/failures are identical on the unfixed base (missing test node_modules in a fresh worktree + a load-sensitive fault-injection timeout).test/expectations.txtuntouched.Note: #34432 also touches
_http_server.ts/NodeHTTP.cppfor different defects (no overlap on these symptoms); textual merge conflicts are possible.