Skip to content

node:https: add addContext and other tls.Server methods to https.Server - #32435

Open
robobun wants to merge 4 commits into
mainfrom
farm/88a7a915/https-server-addcontext
Open

node:https: add addContext and other tls.Server methods to https.Server#32435
robobun wants to merge 4 commits into
mainfrom
farm/88a7a915/https-server-addcontext

Conversation

@robobun

@robobun robobun commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Fixes #12157.
Fixes #24474.

https.Server was a direct alias for http.Server, so none of the tls.Server methods (addContext, setSecureContext, getTicketKeys, setTicketKeys) were available on servers created via https.createServer(). In Node.js, https.Server extends tls.Server, which provides these.

Reproduction

import https from "https";

const server = https.createServer({ ca: "", cert: "", key: "" });
server.addContext("my-domain.com", { ca: "", cert: "", key: "" });
TypeError: server.addContext is not a function. (In 'server.addContext("my-domain.com", {...})', 'server.addContext' is undefined)

Fix

  • https.Server is now its own class extending http.Server (so httpsServer instanceof http.Server is still true, and http.Server does not gain these methods).
  • addContext(hostname, context) accepts the same options as the constructor (key/cert/ca, pfx, minVersion/maxVersion, secureProtocol, ciphers, per-context requestCert/rejectUnauthorized; an empty hostname throws ERR_TLS_REQUIRED_SERVER_NAME like Node) and buffers the context. When listen() is called, the buffered SNI contexts are passed to Bun.serve via the tls array so each serverName is matched via SNI. When called after listen(), a new native binding (httpServerAddServerName) registers the SNI context on the running uWS SSL app via uws_add_server_name_with_options and reinstalls routes for that domain.
  • setSecureContext(options) rebuilds the default TLS config used at listen() from the options given (an omitted option is cleared, as in Node), keeping the server's requestCert/rejectUnauthorized; like Node, providing ca alone does not start requiring a client certificate. Nothing is applied unless every option validated.
  • The constructor, addContext() and setSecureContext() share one option pipeline (serverTlsFromOptions in internal/tls, extracted from http.Server's constructor), so the three cannot drift apart again. https.Server marks itself TLS before http.Server runs, so it gets a real config even when constructed without certificates.
  • getTicketKeys() / setTicketKeys() are stubbed the same way as on tls.Server. Also fixed the existing "implented" typo in the tls.Server stubs.

Verification

New tests at test/js/node/http/node-https-server-context.test.ts:

  • https.Server exposes all four methods and is an http.Server subclass
  • addContext before listen(): connecting with SNI "a.example.com" and "b.example.com" returns the respective per-hostname certs (CN agent1 / agent3); an unknown SNI falls through to the default cert (CN agent2)
  • addContext after listen(): same SNI behavior on a running server, plus an HTTPS request still reaches the request handler
  • setSecureContext before listen() replaces the default cert
  • setSecureContext({key, cert, ca}) on a server created with no initial TLS options does not require a client certificate
  • addContext("") throws before and after listen(), and pfx works through addContext() (both phases) and setSecureContext()
  • setSecureContext() applies minVersion, clears a minVersion the constructor had set, rejects an unknown secureProtocol without applying anything, and keeps the constructor's client certificate policy (a client with a certificate from the configured CA is served the new certificate, one without is refused)

All tests fail on main (TypeError: server.addContext is not a function) and pass with this change. Existing test/js/node/tls/node-tls-context.test.ts, test/js/node/tls/node-tls-server.test.ts, test/js/node/http/node-https-checkServerIdentity.test.ts, and the test-https-* Node parallel tests still pass.

Supersedes #31095 by @sam-shridhar1950f, which introduced the same https.Server subclass split with method stubs (credited with a Co-authored-by trailer on the commit); this PR additionally wires addContext to the underlying SNI mechanism, both at listen time and on a running server, so the contexts actually select per-hostname certificates. Making https.Server a distinct class also fixes the instanceof confusion from #31125 (supertest <= 6.1.6 and @astrojs/node treat every http.Server as HTTPS on Bun).

Known limitations (unchanged from the original review)

  • addContext() / setSecureContext() take plain { key, cert, ca, ... } objects; a tls.createSecureContext() result is not accepted, because Bun.serve's TLS config is built from PEM material and cannot adopt an existing SSL_CTX. Supporting it needs native plumbing shared with tls.Server and is left as a follow-up.
  • setSecureContext() after listen() only affects the next listen(), the same as Bun's tls.Server today; there is no uWS primitive for swapping a running app's default context. addContext() after listen() does take effect.

Rebase notes (current main)

  • https.createServer() on main had grown the ALPN defaulting from Node's https.Server constructor; that logic now lives in the new Server constructor (as in Node), and createServer() returns new Server(...), so new https.Server() gets the same defaults.
  • The post-listen path passes apply_client_cert_policy = true to add_server_name_with_options, matching what the listen-time tls array entries get since Bun.serve: honor requestCert/rejectUnauthorized on per-serverName tls entries #36174, so a context's requestCert / rejectUnauthorized behave the same whether it was added before or after listen().
  • App::remove_server_name (the uws_remove_server_name binding, removed as unused in Remove ~39k lines of dead Rust across the workspace #35002) is restored since it now has a caller.
  • The test helper reads the response through https.get instead of parsing raw bytes, since the server now sends the writeHead(); end("ok") body chunked.
  • The instanceof coverage from node:https: expose tls.Server API on https.Server #31095 is carried over as a test here (http.createServer() is not an https.Server, http.Server does not gain addContext).
  • Two review findings after the rebase: setSecureContext() used to update the live config in place (a rejected option left a half-applied config; the test for it fails against that version with KEY_VALUES_MISMATCH at listen), and both methods still used the option list from before node:http/https/http2: raise Node v26.3.0 compat to ~94%, sync the upstream suites, and fix the Windows/macOS transport-layer teardown bugs they exposed #32488, so pfx, minVersion/maxVersion, secureProtocol and ciphers were silently dropped (a pfx context registered with no certificate). Both are fixed by the shared helper above; ERR_TLS_REQUIRED_SERVER_NAME is added to ErrorCode.ts for the empty-hostname case, and the native binding rejects an empty name like Listener::add_server_name does. tls.Server#addContext() gets the same up-front check (it used to accept "" and fail later at listen()), covered in test/js/node/tls/node-tls-context.test.ts.
  • Later review round: after a successful add_server_name_with_options the post-listen path had two dead error-queue checks that returned before set_routes() (which would have left the hostname registered with an empty router); they are removed, and a failed HTTP/3 registration now removes the entry it just added to the TCP app. tls.Server#addContext() and the addContext dedup loop (ArrayPrototypeSplice) were tidied in the same rounds.
  • Verified: test/js/node/http/node-https-server-context.test.ts (15/15, all fail on main), node-http.test.ts, node-https-checkServerIdentity.test.ts, node-http-agent-tls-options.test.mts, node-tls-context.test.ts, node-tls-server.test.ts, bun-serve-ssl.test.ts, and all 76 test-https-* Node parallel tests; the only failures seen also fail on main in this environment (proxy env vars, localhost resolving to ::1). Also 15/15 on Windows x64 (debug build). The minVersion test reads the refused handshake through a try/catch helper rather than expect().rejects: the latter spins a nested event loop, which on Windows hits a pre-existing usockets crash that reproduces on main with a plain https.createServer({ minVersion }) (the libuv backend never sets tick_depth, so a nested tick frees the socket whose callback is still on the stack); reported separately.

[review] gate passed · iteration 12 · 14 files touched

fails on main (without fix)
ASAN without fix: 16 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/node/http/node-https-server-context.test.ts test/js/node/tls/node-tls-context.test.ts
bun test v1.4.0 (2233ef2f6)

test/js/node/tls/node-tls-context.test.ts:
(pass) tls.Server > addContext [862.37ms]
146 |     const requiredServerName = expect.objectContaining({
147 |       code: "ERR_TLS_REQUIRED_SERVER_NAME",
148 |       message: '"servername" is required parameter for Server.addContext',
149 |     });
150 |     try {
151 |       expect(() => server.addContext("", context)).toThrow(requiredServerName);
                                                         ^
error: expect(received).toThrow(expected)

Expected constructor: ExpectObjectContaining

Received function did not throw
Received value: undefined

      at <anonymous> (/workspace/bun/test/js/node/tls/node-tls-context.test.ts:151:52)
      at <anonymous> (/workspace/bun/test/js/node/tls/node-tls-context.test.ts:143:66)
(fail) tls.Server > addContext rejects an empty hostname up front, like Node [26.48ms]
(pass) tls.Server > should select the most recently added SecureC
... (truncated)

release without fix: 16 FAILED
bun test v1.4.0-canary.1 (eabb96de7)

test/js/node/tls/node-tls-context.test.ts:
(pass) tls.Server > addContext [31.98ms]
146 |     const requiredServerName = expect.objectContaining({
147 |       code: "ERR_TLS_REQUIRED_SERVER_NAME",
148 |       message: '"servername" is required parameter for Server.addContext',
149 |     });
150 |     try {
151 |       expect(() => server.addContext("", context)).toThrow(requiredServerName);
                                                         ^
error: expect(received).toThrow(expected)

Expected constructor: ExpectObjectContaining

Received function did not throw
Received value: undefined

      at <anonymous> (/workspace/bun/test/js/node/tls/node-tls-context.test.ts:151:52)
(fail) tls.Server > addContext rejects an empty hostname up front, like Node [0.81ms]
(pass) tls.Server > should select the most recently added SecureContext [7.95ms]
(pass) tls.Server > should allow multiple CA [4.70ms]
(pass) tls.Server > should allow multiple CA in newline-separated strings [3.61ms]
(pass) tls.Server > SNI tls.Server + tls.connect [16.26ms]
(pass) Bun.serve SNI > single SNI [21.33ms]
(pass) Bun.serve SNI > multiple SNI [11.29ms]
(pass
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/node/http/node-https-server-context.test.ts test/js/node/tls/node-tls-context.test.ts
bun test v1.4.0 (2233ef2f6)

test/js/node/tls/node-tls-context.test.ts:
(pass) tls.Server > addContext [928.65ms]
(pass) tls.Server > addContext rejects an empty hostname up front, like Node [33.10ms]
(pass) tls.Server > should select the most recently added SecureContext [180.91ms]
(pass) tls.Server > should allow multiple CA [180.95ms]
(pass) tls.Server > should allow multiple CA in newline-separated strings [60.32ms]
(pass) tls.Server > SNI tls.Server + tls.connect [303.83ms]
(pass) Bun.serve SNI > single SNI [439.24ms]
(pass) Bun.serve SNI > multiple SNI [202.13ms]
(pass) server certificate chain built from `ca` > presents an intermediate known only to the default store (NODE_EXTRA_CA_CERTS) [3108.34ms]
(pass) server certificate chain built from `ca` > does not present `ca` entries unrelated to the leaf's issuer chain [126.90ms]
(pass) server certificate chain built from `ca` > presents the issuer path when the leaf and intermediate are lo
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 1392ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/93] gen ErrorCode+*.h
[2/40] gen generated_host_exports.rs
generated_host_exports.rs: 92 exports (host=3, lazy=10, generic=79, rust=0); 241 extern-C blocks audited
[3/40] gen cpp.rs (cppbind)
[4/40] gen JS modules (bundle-modules)
Preprocess modules (12023ms)
Bundle modules (75ms)
Postprocesss modules (261ms)
Bundle Functions (1076ms)
Generate Code (42ms)

[13.50s] Bundled "src/js" for production
  2634 kb
  197 internal modules
  13 native modules
  91 internal functions across 17 files
[4/30] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

�[1m�[92m   Compiling�[0m bun_uws_sys v0.0.0 (/workspace/bun/src/uws_sys)
�[1m�[92m   Compiling�[0m bun_io v0.0.0 (/workspace/bun/src/io)
�[1m�[92m   Compiling�[0m bun_uws v0.0.0 (/workspace/bun/src/uws)
�[1m�[92m   Compiling�[0m bun_zlib v0.0.0 (/workspace/bun/src/zlib)
�[1m�[92m   Compiling�[0m bun_event_loop v0.0.0 (/workspace/bun/src/event
... (truncated)
diff hotspot
packages/bun-usockets/src/crypto/openssl.c         |  10 +
 packages/bun-usockets/src/libusockets.h            |   1 +
 packages/bun-uws/src/App.h                         |   8 +
 src/js/internal/http.ts                            |   2 +
 src/js/internal/tls.ts                             | 103 ++++++
 src/js/node/_http_server.ts                        | 117 +------
 src/js/node/https.ts                               |  92 ++++-
 src/js/node/tls.ts                                 |   7 +-
 src/jsc/bindings/ErrorCode.ts                      |   1 +
 src/runtime/node/node_http_binding.rs              |  36 +-
 src/runtime/server/server_body.rs                  | 106 ++++++
 src/uws_sys/App.rs                                 |  16 +
 .../js/node/http/node-https-server-context.test.ts | 386 +++++++++++++++++++++
 test/js/node/tls/node-tls-context.test.ts          |  20 ++
 14 files changed, 795 insertions(+), 110 deletions(-)

gate history · 5 passed · 1 rejected · iteration 12

evidence per changed file
file                                                 reads  edits  tests
packages/bun-usockets/src/crypto/openssl.c               1      1     37
packages/bun-usockets/src/libusockets.h                  1      1     37
packages/bun-uws/src/App.h                               1      1     37
src/js/internal/http.ts                                  2      2     37
src/js/internal/tls.ts                                   2      0     37
src/js/node/_http_server.ts                              6      5     37
src/js/node/https.ts                                    16     26     37
src/js/node/tls.ts                                       6      3     37
src/jsc/bindings/ErrorCode.ts                            0      0     37
src/runtime/node/node_http_binding.rs                    3      3     37
src/runtime/server/server_body.rs                        9      6     37
src/uws_sys/App.rs                                       1      0     37
test/js/node/http/node-https-server-context.test.ts     11     17     28
test/js/node/tls/node-tls-context.test.ts                3      1     12

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds SNI (Server Name Indication) context support to Bun's HTTPS server by introducing a kSNIContexts symbol, a dedicated https.Server constructor with addContext and setSecureContext methods, TLS array assembly at listen time, and native binding/runtime support for dynamic SNI context registration after the server is listening.

Changes

HTTPS Server SNI Context Support

Layer / File(s) Summary
kSNIContexts symbol declaration
src/js/internal/http.ts
Declares and exports the kSNIContexts symbol for storing per-server SNI context entries.
https.Server constructor with SNI methods
src/js/node/https.ts, src/js/node/tls.ts
Introduces a new https.Server constructor that marks instances as TLS servers, normalizes TLS options via tlsOptionsFromContext, and adds addContext, setSecureContext, and stubbed ticket-key prototype methods. Updates createServer and the exported https object to use the new constructor. Corrects spelling in tls.Server ticket-key error messages for consistency.
SNI TLS array assembly in kRealListen
src/js/node/_http_server.ts
Imports kSNIContexts and extends kRealListen to detect non-empty SNI contexts on the server instance, prepend a base/default TLS entry, and append each SNI context into a TLS array for Bun.serve.
Native binding and runtime SNI support
src/runtime/node/node_http_binding.rs, src/runtime/server/server_body.rs
Adds http_server_add_server_name JS binding to dispatch SNI context registration to the appropriate server type. Implements add_sni_context method on NewServer to register per-host TLS contexts at runtime: validates SSL/listening state, parses TLS options, installs server names on uWS and H3 apps, and reinstalls routes.
Integration tests for https.Server SNI
test/js/node/http/node-https-server-context.test.ts
Adds test suite with helpers to load PEM fixtures and extract peer certificate subject CN via tls.connect. Asserts method exposure (addContext, setSecureContext, getTicketKeys, setTicketKeys), http.Server instanceof hierarchy, per-host certificate selection via addContext before/after listen, and default certificate replacement via setSecureContext.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The pull request fully addresses the linked issues. Issue #12157 requests addContext support on https.Server, and issue #24474 requests https.Server inherit tls.Server methods. The implementation adds all four methods (addContext, setSecureContext, getTicketKeys, setTicketKeys) with working SNI functionality.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing https.Server methods and SNI support. The typo fix in tls.Server is a minor, closely related improvement. No extraneous or unrelated code modifications are present.
Description check ✅ Passed The description explains the change, linked issues, implementation, verification, and known limitations.
Title check ✅ Passed The title clearly identifies the main change: adding TLS server methods to node:https Server.

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

@robobun

robobun commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 1:25 PM PT - Aug 16th, 2026

@robobun, your commit 2233ef2 has 4 failures in Build #99491 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 32435

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

bun-32435 --bun

@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. [node compatibility] https.Server should inherit from tls.Server #24474 - PR makes https.Server a proper subclass with tls.Server methods (addContext, setSecureContext, etc.), directly fixing the reported problem that https.createServer().addContext is undefined

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #24474

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. node:https: expose tls.Server API on https.Server #31095 - Same goal: makes https.Server its own class extending http.Server and exposes addContext, setSecureContext, getTicketKeys/setTicketKeys on it

🤖 Generated with Claude Code

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/js/node/https.ts`:
- Around line 151-157: The error messages in both the getTicketKeys and
setTicketKeys methods contain a typo where "implented" should be spelled as
"implemented". Update the Error messages in both Server.prototype.getTicketKeys
and Server.prototype.setTicketKeys to correct the spelling from "Not implented
in Bun yet" to "Not implemented in Bun yet".
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a1bf7bd6-4259-40b8-af3d-7a43ec5bed54

📥 Commits

Reviewing files that changed from the base of the PR and between 91ce3b4 and 0da5f6d.

📒 Files selected for processing (4)
  • src/js/internal/http.ts
  • src/js/node/_http_server.ts
  • src/js/node/https.ts
  • test/js/node/http/node-https-server-context.test.ts

Comment thread src/js/node/https.ts
Comment thread src/js/node/https.ts Outdated
Comment thread src/js/node/https.ts
Comment thread src/js/node/https.ts Outdated
Comment thread src/js/node/https.ts
@robobun
robobun force-pushed the farm/88a7a915/https-server-addcontext branch from 7b35335 to a028e90 Compare June 17, 2026 16:30

@coderabbitai coderabbitai 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.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/js/node/https.ts`:
- Around line 84-91: The validation checks in the tlsOptionsFromContext function
and related TLS option validations use truthiness checks (if cert, if key, if
ca, if passphrase) that allow invalid falsy values like 0, false, and empty
strings to bypass type validation. Replace these truthiness checks with explicit
null and undefined checks instead. For example, change "if (passphrase && typeof
passphrase !== 'string')" to "if (passphrase != null && typeof passphrase !==
'string')" for the passphrase validation on line 89, and apply the same pattern
to all other TLS option validations including cert, key, and ca. Also apply this
same fix pattern to the similar validation logic mentioned at lines 140-153.

In `@src/js/node/tls.ts`:
- Around line 622-635: The function tlsStringToProtocolVersion() returns 0 for
invalid version strings, but this invalid result is not validated after the
conversion. In the newNativeSecureContext() function around lines 743-747 and in
the TLSSocket path around lines 1790-1801, add validation checks after each call
to tlsStringToProtocolVersion() that verify the returned value is not 0. If the
result is 0 (indicating an invalid protocol version string was passed), throw an
ERR_TLS_INVALID_PROTOCOL_VERSION error instead of silently using the invalid
value, to match Node.js behavior and prevent invalid strings like "invalid" from
being treated as valid protocol versions.

In `@src/runtime/server/server_body.rs`:
- Around line 2553-2564: The H3 SNI registration error handling in the
`Self::HAS_H3` block is incomplete because it only returns an error when
`add_server_name_with_options` fails AND `global.has_exception()` is false. This
causes the function to silently continue when an exception is already pending.
Remove the `&& !global.has_exception()` condition from the if statement so that
any failure from `add_server_name_with_options().is_err()` always returns an
error. Additionally, add a separate check after the if block to return
`Err(JsError::Thrown)` if `global.has_exception()` is true at that point,
mirroring the H1 error path pattern and ensuring no failures are swallowed.

In `@test/js/node/http/node-https-server-context.test.ts`:
- Around line 23-30: The peerCN function establishes an SNI connection but only
checks the peer certificate, not whether the correct route was actually selected
by SNI. Enhance the function to send an actual HTTP GET request over the TLS
socket after the secureConnect event completes, read and return the response
body instead of just the certificate CN, and ensure the caller asserts this
response matches the expected body for the a.example.com SNI-selected route.
This will prove the test fails when SNI routing is not working correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3e0185b7-de54-473b-b108-47eea20c3ae6

📥 Commits

Reviewing files that changed from the base of the PR and between 0da5f6d and a028e90.

📒 Files selected for processing (7)
  • src/js/internal/http.ts
  • src/js/node/_http_server.ts
  • src/js/node/https.ts
  • src/js/node/tls.ts
  • src/runtime/node/node_http_binding.rs
  • src/runtime/server/server_body.rs
  • test/js/node/http/node-https-server-context.test.ts

@coderabbitai coderabbitai 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.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/js/node/https.ts`:
- Around line 84-91: The validation checks in the tlsOptionsFromContext function
and related TLS option validations use truthiness checks (if cert, if key, if
ca, if passphrase) that allow invalid falsy values like 0, false, and empty
strings to bypass type validation. Replace these truthiness checks with explicit
null and undefined checks instead. For example, change "if (passphrase && typeof
passphrase !== 'string')" to "if (passphrase != null && typeof passphrase !==
'string')" for the passphrase validation on line 89, and apply the same pattern
to all other TLS option validations including cert, key, and ca. Also apply this
same fix pattern to the similar validation logic mentioned at lines 140-153.

In `@src/js/node/tls.ts`:
- Around line 622-635: The function tlsStringToProtocolVersion() returns 0 for
invalid version strings, but this invalid result is not validated after the
conversion. In the newNativeSecureContext() function around lines 743-747 and in
the TLSSocket path around lines 1790-1801, add validation checks after each call
to tlsStringToProtocolVersion() that verify the returned value is not 0. If the
result is 0 (indicating an invalid protocol version string was passed), throw an
ERR_TLS_INVALID_PROTOCOL_VERSION error instead of silently using the invalid
value, to match Node.js behavior and prevent invalid strings like "invalid" from
being treated as valid protocol versions.

In `@src/runtime/server/server_body.rs`:
- Around line 2553-2564: The H3 SNI registration error handling in the
`Self::HAS_H3` block is incomplete because it only returns an error when
`add_server_name_with_options` fails AND `global.has_exception()` is false. This
causes the function to silently continue when an exception is already pending.
Remove the `&& !global.has_exception()` condition from the if statement so that
any failure from `add_server_name_with_options().is_err()` always returns an
error. Additionally, add a separate check after the if block to return
`Err(JsError::Thrown)` if `global.has_exception()` is true at that point,
mirroring the H1 error path pattern and ensuring no failures are swallowed.

In `@test/js/node/http/node-https-server-context.test.ts`:
- Around line 23-30: The peerCN function establishes an SNI connection but only
checks the peer certificate, not whether the correct route was actually selected
by SNI. Enhance the function to send an actual HTTP GET request over the TLS
socket after the secureConnect event completes, read and return the response
body instead of just the certificate CN, and ensure the caller asserts this
response matches the expected body for the a.example.com SNI-selected route.
This will prove the test fails when SNI routing is not working correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3e0185b7-de54-473b-b108-47eea20c3ae6

📥 Commits

Reviewing files that changed from the base of the PR and between 0da5f6d and a028e90.

📒 Files selected for processing (7)
  • src/js/internal/http.ts
  • src/js/node/_http_server.ts
  • src/js/node/https.ts
  • src/js/node/tls.ts
  • src/runtime/node/node_http_binding.rs
  • src/runtime/server/server_body.rs
  • test/js/node/http/node-https-server-context.test.ts
🛑 Comments failed to post (4)
src/js/node/https.ts (1)

84-91: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Falsy values bypass TLS option type validation.

Line 89 and Lines 140-153 use truthiness checks (if (value && typeof value !== ...)), so invalid falsy values like 0, false, and "" pass validation and get written into TLS config.

Suggested fix
-  if (passphrase && typeof passphrase !== "string") {
+  if (passphrase !== undefined && passphrase !== null && typeof passphrase !== "string") {
     throw $ERR_INVALID_ARG_TYPE("options.passphrase", "string", passphrase);
   }
@@
-    if (passphrase && typeof passphrase !== "string") {
+    if (passphrase !== undefined && passphrase !== null && typeof passphrase !== "string") {
       throw $ERR_INVALID_ARG_TYPE("options.passphrase", "string", passphrase);
     }
@@
-    if (servername && typeof servername !== "string") {
+    if (servername !== undefined && servername !== null && typeof servername !== "string") {
       throw $ERR_INVALID_ARG_TYPE("options.servername", "string", servername);
     }
@@
-    if (secureOptions && typeof secureOptions !== "number") {
+    if (secureOptions !== undefined && secureOptions !== null && typeof secureOptions !== "number") {
       throw $ERR_INVALID_ARG_TYPE("options.secureOptions", "number", secureOptions);
     }

Also applies to: 139-154

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/js/node/https.ts` around lines 84 - 91, The validation checks in the
tlsOptionsFromContext function and related TLS option validations use truthiness
checks (if cert, if key, if ca, if passphrase) that allow invalid falsy values
like 0, false, and empty strings to bypass type validation. Replace these
truthiness checks with explicit null and undefined checks instead. For example,
change "if (passphrase && typeof passphrase !== 'string')" to "if (passphrase !=
null && typeof passphrase !== 'string')" for the passphrase validation on line
89, and apply the same pattern to all other TLS option validations including
cert, key, and ca. Also apply this same fix pattern to the similar validation
logic mentioned at lines 140-153.
src/js/node/tls.ts (1)

622-635: ⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check the actual state of src/js/node/tls.ts around the flagged lines
sed -n '620,650p' src/js/node/tls.ts
sed -n '740,750p' src/js/node/tls.ts
sed -n '1785,1805p' src/js/node/tls.ts

Repository: oven-sh/bun

Length of output: 2129


🏁 Script executed:

# Find where DEFAULT_MIN_VERSION and DEFAULT_MAX_VERSION are defined and exported
rg 'DEFAULT_MIN_VERSION|DEFAULT_MAX_VERSION' src/js/node/tls.ts -B2 -A2

Repository: oven-sh/bun

Length of output: 2635


🏁 Script executed:

# Check if these constants are exported and writable
rg 'export.*DEFAULT_MIN_VERSION|export.*DEFAULT_MAX_VERSION' src/js/node/tls.ts

Repository: oven-sh/bun

Length of output: 37


🏁 Script executed:

# Look at the Node.js TLS spec behavior for invalid protocol versions
# Check what the native layer expects
rg 'tlsStringToProtocolVersion|TLS1_VERSION' src/js/node/tls.ts -B2 -A2 | head -60

Repository: oven-sh/bun

Length of output: 1193


🏁 Script executed:

# Check if there are tests for invalid TLS versions
find test -name "*.test.ts" -o -name "*.test.js" | xargs rg -l "tls|TLS" | head -5

Repository: oven-sh/bun

Length of output: 242


🏁 Script executed:

# Find test files specifically for node:tls
fd -e test.ts -e test.js | rg tls

Repository: oven-sh/bun

Length of output: 1663


🏁 Script executed:

# Look at the node-tls-create-secure-context-args test
head -100 test/js/node/tls/node-tls-create-secure-context-args.test.ts

Repository: oven-sh/bun

Length of output: 3074


🏁 Script executed:

# Search for tests that explicitly test invalid version strings
rg 'TLSv|minVersion|maxVersion' test/js/node/tls/node-tls-create-secure-context-args.test.ts -A3 -B1 | head -80

Repository: oven-sh/bun

Length of output: 37


🏁 Script executed:

# Check if there's validation in the native layer by searching for error handling
rg 'minVersion|maxVersion' src/runtime -A2 -B2 | head -100

Repository: oven-sh/bun

Length of output: 1497


🏁 Script executed:

# Look for what happens when 0 is passed as version
rg 'set_min_proto_version|set_max_proto_version|minVersion.*0' src/ -r -A2 -B2 | head -80

Repository: oven-sh/bun

Length of output: 1165


🏁 Script executed:

# Search for how the native layer handles minVersion/maxVersion, especially 0
rg 'ssl_min_version|ssl_max_version' src/ -A3 -B3 | head -100

Repository: oven-sh/bun

Length of output: 5931


🏁 Script executed:

# Check if there's any validation when a version is 0
rg 'minVersion.*0|maxVersion.*0' src/runtime -B2 -A2 | head -60

Repository: oven-sh/bun

Length of output: 37


🏁 Script executed:

# Look for the actual native code that sets protocol versions
rg 'SSL_CTX_set_min_proto_version|SSL_CTX_set_max_proto_version' src/ -B3 -A3 | head -80

Repository: oven-sh/bun

Length of output: 915


🏁 Script executed:

# Search for what happens with the 0 return value from tlsStringToProtocolVersion
rg 'tlsStringToProtocolVersion' src/js/node/tls.ts -A5 -B5

Repository: oven-sh/bun

Length of output: 1370


🏁 Script executed:

# Check Node.js behavior: does it validate when you set tls.DEFAULT_MIN_VERSION?
# Look at existing tests for setting these defaults
rg 'DEFAULT_MIN_VERSION|DEFAULT_MAX_VERSION' test/js/node/tls/ -B3 -A3

Repository: oven-sh/bun

Length of output: 1178


🏁 Script executed:

# Check if the native layer actually has defaults for when 0 is passed
rg 'ssl_min_version.*0|ssl_max_version.*0' src/http/ -B5 -A5

Repository: oven-sh/bun

Length of output: 681


🏁 Script executed:

# Search for where BoringSSL gets called with these versions
rg 'SSL_CTX_set_min_proto_version|SSL_CTX_set_max_proto_version' src/ --type cpp --type rs -B2 -A2 | head -60

Repository: oven-sh/bun

Length of output: 82


🏁 Script executed:

# Check what Node.js actually does - look at the github link in the comment
rg 'secureProtocolToVersionRange|Node seeds the protocol' src/js/node/tls.ts -B5 -A15

Repository: oven-sh/bun

Length of output: 3637


🏁 Script executed:

# Check if there are any tests setting invalid version strings to DEFAULT_MIN/MAX_VERSION
rg 'DEFAULT_MIN_VERSION|DEFAULT_MAX_VERSION' test/js/node/tls -A10 | grep -A10 'invalid\|throw\|error'

Repository: oven-sh/bun

Length of output: 37


🏁 Script executed:

# Look at Node.js source to understand what happens with invalid strings
# Check if Node validates the version string when DEFAULT_MIN_VERSION is set
# First, let's see what the actual behavior is supposed to be
node -e "const tls = require('tls'); tls.DEFAULT_MIN_VERSION = 'invalid'; console.log(tls.DEFAULT_MIN_VERSION); const ctx = tls.createSecureContext(); console.log('created context');" 2>&1 || true

Repository: oven-sh/bun

Length of output: 825


🏁 Script executed:

# Since we can't run Node.js in this environment, check what the review expects
# Let's see if there's any validation elsewhere in the code
rg 'ERR_TLS_INVALID_PROTOCOL_VERSION' src/

Repository: oven-sh/bun

Length of output: 1096


🏁 Script executed:

# Look at what happens when 0 is actually passed to the native layer
# Check the C++ bindings that consume these values
fd -e cpp -e h | xargs rg 'ssl_min_version|ssl_max_version' 2>/dev/null | head -40

Repository: oven-sh/bun

Length of output: 305


🏁 Script executed:

# Check the exact lines referenced in the review to see current state
sed -n '622,635p' src/js/node/tls.ts

Repository: oven-sh/bun

Length of output: 338


🏁 Script executed:

# Check if validation is already present in the file
rg '\$ERR_TLS_INVALID_PROTOCOL_VERSION' src/js/node/tls.ts -B5 -A2

Repository: oven-sh/bun

Length of output: 700


🏁 Script executed:

# Get the full context of where validation happens
sed -n '740,765p' src/js/node/tls.ts

Repository: oven-sh/bun

Length of output: 1059


🏁 Script executed:

# Find VALID_TLS_VERSIONS definition
rg 'VALID_TLS_VERSIONS' src/js/node/tls.ts -B5 -A5

Repository: oven-sh/bun

Length of output: 1344


🏁 Script executed:

# Check the full context of the newNativeSecureContext function to see where validation happens
sed -n '700,780p' src/js/node/tls.ts

Repository: oven-sh/bun

Length of output: 3847


🏁 Script executed:

# Check the third location mentioned in "Also applies to: 743-747, 1790-1801"
sed -n '1785,1810p' src/js/node/tls.ts

Repository: oven-sh/bun

Length of output: 737


Validate translated TLS protocol versions; invalid DEFAULT_MIN/MAX_VERSION strings silently degrade bounds.

When tlsStringToProtocolVersion() receives an invalid version string (including from DEFAULT_MIN_VERSION / DEFAULT_MAX_VERSION), it returns 0. The current code in newNativeSecureContext() (lines 743–747) converts these strings to numeric protocol versions but never validates the result. If a developer sets tls.DEFAULT_MIN_VERSION to an invalid string (e.g., tls.DEFAULT_MIN_VERSION = "invalid"), and createSecureContext() is called without explicit minVersion, the invalid string silently converts to 0 and disables the version bound instead of throwing.

Node.js rejects this with ERR_TLS_INVALID_PROTOCOL_VERSION at context creation time. Add validation after each string-to-number conversion to match that behavior:

Suggested fix
       } else {
         minVersion = tlsStringToProtocolVersion(optMinVersion ?? DEFAULT_MIN_VERSION);
         maxVersion = tlsStringToProtocolVersion(optMaxVersion ?? DEFAULT_MAX_VERSION);
+        if (minVersion === 0) throw $ERR_TLS_INVALID_PROTOCOL_VERSION(String(optMinVersion ?? DEFAULT_MIN_VERSION), "minimum");
+        if (maxVersion === 0) throw $ERR_TLS_INVALID_PROTOCOL_VERSION(String(optMaxVersion ?? DEFAULT_MAX_VERSION), "maximum");
       }

Also applies to lines 1790–1801 in the TLSSocket path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/js/node/tls.ts` around lines 622 - 635, The function
tlsStringToProtocolVersion() returns 0 for invalid version strings, but this
invalid result is not validated after the conversion. In the
newNativeSecureContext() function around lines 743-747 and in the TLSSocket path
around lines 1790-1801, add validation checks after each call to
tlsStringToProtocolVersion() that verify the returned value is not 0. If the
result is 0 (indicating an invalid protocol version string was passed), throw an
ERR_TLS_INVALID_PROTOCOL_VERSION error instead of silently using the invalid
value, to match Node.js behavior and prevent invalid strings like "invalid" from
being treated as valid protocol versions.
src/runtime/server/server_body.rs (1)

2553-2564: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Propagate H3 SNI registration failures instead of falling through.

When the H3 add_server_name_with_options call fails while global.has_exception() is already true, this condition is false and the method continues to set_routes() and returns success with a pending exception. Mirror the H1 error path and return Err(JsError::Thrown) whenever the H3 registration fails or leaves an SSL/JS error pending. As per coding guidelines, “Never swallow a failure or signal success on one.”

🐛 Proposed fix
         if Self::HAS_H3 {
             if let Some(h3_app) = self.h3_app {
-                if bun_opaque::opaque_deref_mut(h3_app)
+                if bun_opaque::opaque_deref_mut(h3_app)
                     .add_server_name_with_options(z, &ssl_opts)
                     .is_err()
-                    && !global.has_exception()
                 {
+                    if !global.has_exception() && !super::throw_ssl_error_if_necessary(global) {
+                        return Err(global.throw(format_args!(
+                            "Failed to add serverName \"{}\" for HTTP/3",
+                            bstr::BStr::new(server_name.to_bytes())
+                        )));
+                    }
+                    return Err(JsError::Thrown);
+                }
+                if super::throw_ssl_error_if_necessary(global) {
+                    return Err(JsError::Thrown);
+                }
-                    return Err(global.throw(format_args!(
-                        "Failed to add serverName \"{}\" for HTTP/3",
-                        bstr::BStr::new(server_name.to_bytes())
-                    )));
-                }
             }
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/server/server_body.rs` around lines 2553 - 2564, The H3 SNI
registration error handling in the `Self::HAS_H3` block is incomplete because it
only returns an error when `add_server_name_with_options` fails AND
`global.has_exception()` is false. This causes the function to silently continue
when an exception is already pending. Remove the `&& !global.has_exception()`
condition from the if statement so that any failure from
`add_server_name_with_options().is_err()` always returns an error. Additionally,
add a separate check after the if block to return `Err(JsError::Thrown)` if
`global.has_exception()` is true at that point, mirroring the H1 error path
pattern and ensuring no failures are swallowed.

Source: Coding guidelines

test/js/node/http/node-https-server-context.test.ts (1)

23-30: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Exercise the SNI-selected route, not only the Host header.

fetch("https://127.0.0.1:...") does not use a.example.com as TLS SNI; the Host header is sent after TLS context selection. This assertion can pass through the default route even if the new SNI domain never gets routes installed. Reuse tls.connect({ host: "127.0.0.1", port, servername: "a.example.com" }), send an HTTP request over that socket, and assert the response body. As per coding guidelines, “Prove the test fails for the RIGHT reason” and “Every assertion must be able to fail, and assert the strongest invariant.”

🧪 Proposed test helper shape
 async function peerCN(port: number, servername?: string) {
   const socket = tls.connect({ host: "127.0.0.1", port, servername, rejectUnauthorized: false });
   const errored = once(socket, "error");
   await Promise.race([once(socket, "secureConnect"), errored.then(([e]) => Promise.reject(e))]);
   const cert = socket.getPeerCertificate();
   socket.destroy();
   return cert.subject?.CN;
 }
+
+async function httpsBodyViaSNI(port: number, servername: string) {
+  const socket = tls.connect({ host: "127.0.0.1", port, servername, rejectUnauthorized: false });
+  const errored = once(socket, "error").then(([e]) => Promise.reject(e));
+  try {
+    await Promise.race([once(socket, "secureConnect"), errored]);
+    socket.write(`GET / HTTP/1.1\r\nHost: ${servername}\r\nConnection: close\r\n\r\n`);
+
+    const chunks: Buffer[] = [];
+    socket.on("data", chunk => chunks.push(chunk));
+    await Promise.race([once(socket, "end"), once(socket, "close"), errored]);
+
+    const response = Buffer.concat(chunks).toString("utf8");
+    return response.slice(response.indexOf("\r\n\r\n") + 4);
+  } finally {
+    socket.destroy();
+  }
+}
-      const res = await fetch(`https://127.0.0.1:${port}/`, {
-        tls: { rejectUnauthorized: false, checkServerIdentity: () => undefined },
-        headers: { Host: "a.example.com" },
-      });
-      expect(await res.text()).toBe("ok");
+      expect(await httpsBodyViaSNI(port, "a.example.com")).toBe("ok");

Also applies to: 95-99

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/js/node/http/node-https-server-context.test.ts` around lines 23 - 30,
The peerCN function establishes an SNI connection but only checks the peer
certificate, not whether the correct route was actually selected by SNI. Enhance
the function to send an actual HTTP GET request over the TLS socket after the
secureConnect event completes, read and return the response body instead of just
the certificate CN, and ensure the caller asserts this response matches the
expected body for the a.example.com SNI-selected route. This will prove the test
fails when SNI routing is not working correctly.

Source: Coding guidelines

@robobun

robobun commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator Author

Re the four CodeRabbit findings that failed to post inline (review 4517716418), addressed in 34b011c:

  • server_body.rs H3 SNI error propagation: fixed; the H3 path now mirrors the H1 path and returns Err(JsError::Thrown) on any failure instead of falling through when an exception is already pending.
  • Test: verify SNI route, not just cert: fixed; the post-listen test now sends an HTTP request over a tls.connect({ servername }) socket and asserts both the served cert CN and the response body, so it fails if routes are not installed for the new SNI domain.

Declined with reasons:

  • https.ts truthiness checks on cert/key/ca/passphrase: these match the existing validation in _http_server.ts (lines 234-264) and tls.ts exactly; changing only https.ts would diverge from the established convention for the same options.
  • tls.ts tlsStringToProtocolVersion validation: this PR's only change to tls.ts is the "implented" typo fix in the ticket-key stubs. The flagged code (newNativeSecureContext, TLSSocket path) is pre-existing and unrelated to https.Server#addContext.

Comment thread src/js/node/https.ts
Comment thread src/js/node/https.ts Outdated
Comment thread src/js/node/https.ts
@robobun

robobun commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator Author

CI build 63164: 284 passed, 2 failed. Neither failure touches this diff:

  • test/js/bun/http/serve-body-leak.test.ts on debian-13-x64-asan: end_memory 541 MB vs ≤512 MB threshold (~5% over) in the "buffering a JSON body" case; this is a Bun.serve request-body memory threshold test under ASAN and does not exercise node:https.
  • test/js/bun/terminal/terminal.test.ts on darwin-14-x64: "creates subprocess with terminal attached" timed out after 90s; this is Bun.spawn PTY handling.

The new test/js/node/http/node-https-server-context.test.ts suite and the other node:https/node:tls suites pass on every platform. Previous runs on this branch (63153, 63160) failed only on similarly unrelated lanes (autobahn docker platform mismatch, puppeteer download, Windows hot-reload flakes).

Ready for review.

@robobun
robobun force-pushed the farm/88a7a915/https-server-addcontext branch from baa7f22 to 23ef95d Compare June 18, 2026 12:44
Comment thread src/runtime/server/server_body.rs
Comment thread src/runtime/server/server_body.rs
Comment thread src/runtime/server/server_body.rs
Comment thread test/js/node/http/node-https-server-context.test.ts
@robobun

robobun commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased on main and squashed to one commit (3ede0f2).

Conflicts resolved in src/js/node/https.ts only: main added validateHeaderValue from node:_http_common to the import block that this PR also extends, so both import lists were merged. Separately, main migrated the JS-to-native binding macro from $newZigFunction("*.zig", ...) to $newRustFunction("*.rs", ...), so httpServerAddServerName now uses $newRustFunction("node_http_binding.rs", ...).

All 7 tests in test/js/node/http/node-https-server-context.test.ts pass with the fix and fail on main. node-tls-context.test.ts, node-https-checkServerIdentity.test.ts, and the test-https-* Node parallel tests still pass.

@robobun
robobun force-pushed the farm/88a7a915/https-server-addcontext branch 2 times, most recently from d42fcd3 to 20921b0 Compare July 5, 2026 06:29
@robobun

robobun commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator Author

CI note (kept current in place):

Head 2233ef2 is the PR rebased onto current main (07d38c1); the only conflict was an import line in _http_server.ts where main removed kDeprecatedReplySymbol next to the kSNIContexts this PR adds.

Build #99491 for it: every lane that has finished is green except two debian-13-x64-asan shards, which fail only on leak-detection tests timing out under ASAN (timers/setInterval.test.js "doesn't leak memory", workerd/html-rewriter-leak.test.ts, cli/run/require-cache.test.ts). None of those load node:https/node:tls or start a server; the same ASAN leak-test timeouts have shown up on earlier builds of this branch (and require-cache has been seen failing on main), and they are reported separately. One windows-11-aarch64 shard later also failed on cli/install/bun-install-registry.test.ts (a bun install version-resolution assertion), equally unrelated and also reported separately. node-https-server-context.test.ts, node-tls-context.test.ts and the other http/https/tls suites pass on every lane that ran them, including the other ASAN shards.

Local, on the rebased debug+ASAN build: node-https-server-context.test.ts + node-tls-context.test.ts 33/33 (0/15 of the https file pass without the change); node-http.test.ts, node-https-checkServerIdentity.test.ts, node-tls-server.test.ts pass apart from the environment-specific cases noted in the description; all 62 test-https-* / tls addContext Node tests pass. Also 15/15 on a Windows x64 debug build earlier in the branch's history.

Nothing left to do here from my side; ready for review.

Comment thread src/runtime/node/node_http_binding.rs Outdated
Comment thread src/js/node/https.ts Outdated
Comment thread src/js/node/https.ts Outdated
Comment thread src/runtime/server/server_body.rs
Comment thread src/runtime/server/server_body.rs
Comment thread src/runtime/server/server_body.rs
Comment thread test/js/node/http/node-https-server-context.test.ts
@robobun
robobun force-pushed the farm/88a7a915/https-server-addcontext branch from 8e9dce9 to 4bf7ca1 Compare August 13, 2026 02:53

@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 latest revision (4bf7ca1) and found no issues — all three prior findings (the OwnedString wrap, the setSecureContext validate-before-commit ordering, and the unhandled closed promise) are addressed in the current diff. Given the scope — a new native binding, SSL_CTX lifecycle changes in App.h/openssl.c, and ~100 lines of Rust in add_sni_context with a manual SSL_CTX_free — a human look at the native memory-safety and TLS changes is still warranted.

What was reviewed:

  • add_sni_context's probe-before-remove ordering: the probe SSL_CTX is freed on success, and every error path returns before remove_server_name mutates state
  • us_internal_ssl_ctx_clear_sni_userdata clears the ex_data slot so keep-alive connections on a removed SNI fall back to the default router instead of dereferencing the freed one; covered by the keep-alive re-add test
  • setSecureContext now validates every option before replacing this[tlsSymbol] in one assignment; covered by the invalid-key test
  • The keep-alive test's closed promise is now in the first Promise.race, so no unhandled rejection on early failure
Extended reasoning...

Overview

This PR makes https.Server a distinct subclass of http.Server and wires addContext, setSecureContext, getTicketKeys, and setTicketKeys onto it. It spans 11 files across four layers: JS built-ins (https.ts, _http_server.ts, internal/http.ts, tls.ts), a new Rust binding (node_http_binding.rsserver_body.rs::add_sni_context), a restored uWS FFI wrapper (App.rs::remove_server_name), and C/C++ changes in vendored uSockets/uWS (openssl.c, libusockets.h, App.h) that add us_internal_ssl_ctx_clear_sni_userdata and call it from removeServerName to prevent a UAF on the per-domain router when a keep-alive connection outlives its SNI context. A 271-line test file exercises pre- and post-listen SNI registration, hostname re-add, malformed-PEM rejection, keep-alive across re-add, and setSecureContext atomicity.

Security risks

The PR is squarely in TLS/SNI territory: it constructs SSL_CTX objects from user-supplied key/cert/CA material, registers them on a running server, and normalizes requestCert/rejectUnauthorized for both the default and per-hostname contexts. The requestCert/rejectUnauthorized normalization in tlsOptionsFromContext and setSecureContext mirrors normalizeServerTls (rejectUnauthorized only applies when requestCert is true), and the post-listen path passes apply_client_cert_policy = true to match listen-time entries. I did not spot a way to weaken verification relative to what Bun.serve's tls-array path already does, but a maintainer familiar with #36174's client-cert-policy semantics should confirm the parity.

Level of scrutiny

High. This is not a mechanical change: it introduces a new native entry point that mutates a live uWS SSL app, adds an unsafe { SSL_CTX_free(probe) } call in Rust, and patches vendored C++ (App.h::removeServerName) to fix a use-after-free that this PR's own re-add path would otherwise expose. The C/C++ change is small and well-commented, and the keep-alive test covers it, but changes to vendored uSockets/uWS memory management are exactly the kind of thing REVIEW.md flags for maintainer sign-off.

Other factors

The PR has been through three prior automated review rounds; each finding (a WTFStringImpl ref leak, a partial-write-on-throw in setSecureContext, a bare .toThrow(), and an unhandled-rejection edge in the keep-alive test) was fixed and is verified in the current diff. The comment-cop bot's flags were addressed by shortening comments. Test coverage is thorough for the JS-visible surface. The two known limitations (no SecureContext object support, post-listen setSecureContext deferred to next listen()) are documented in the description and match tls.Server's current behavior. Deferring to a human is appropriate given the native/TLS surface, not because of any specific unresolved concern.

Comment thread src/js/internal/tls.ts
Comment thread src/js/internal/tls.ts
Comment thread src/js/internal/tls.ts
Comment thread src/js/internal/tls.ts
Comment thread src/js/node/_http_server.ts
Comment thread src/js/node/https.ts
Comment thread src/js/node/https.ts
Comment thread src/runtime/server/server_body.rs
@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-up in 1c01f2e after a self-review of the rebased diff, which turned up two things the rebase had left behind:

  • addContext() / setSecureContext() still validated the option list http.Server had before node:http/https/http2: raise Node v26.3.0 compat to ~94%, sync the upstream suites, and fix the Windows/macOS transport-layer teardown bugs they exposed #32488, so pfx, minVersion/maxVersion, secureProtocol and ciphers were silently dropped by both methods (a pfx context was registered with no certificate, and a minVersion set in the constructor survived a later setSecureContext() while one passed to it was ignored). The constructor's option handling now lives in one internal/tls helper used by all three entry points; https.Server marks itself TLS before http.Server runs, so the hand-made default config is gone, and setSecureContext() rebuilds the config from what it is given and keeps only the server's requestCert/rejectUnauthorized, as Node does.
  • addContext("") was accepted: before listen() it made listen() fail later with SNI tls object must have a serverName, after it the native path registered an entry on the SNI tree root that nothing can match. It now throws ERR_TLS_REQUIRED_SERVER_NAME synchronously in both phases (new code in ErrorCode.ts, same code and message as Node), and the binding itself rejects an empty name the way Listener::add_server_name does.

Six tests added for these (node-https-server-context.test.ts is 15/15 with the change, 0/15 on main); node-http.test.ts, the https/tls suites listed in the description and all 76 test-https-* Node tests are unchanged from main. Description updated to match.

@robobun
robobun force-pushed the farm/88a7a915/https-server-addcontext branch from 1c01f2e to 6259011 Compare August 13, 2026 09:34
Comment thread src/js/node/https.ts
@robobun
robobun force-pushed the farm/88a7a915/https-server-addcontext branch from 6259011 to 1bcd34d Compare August 13, 2026 10:26
Comment thread src/runtime/server/server_body.rs Outdated
Comment thread src/js/node/https.ts
@robobun
robobun force-pushed the farm/88a7a915/https-server-addcontext branch from a15fb2a to f623106 Compare August 13, 2026 17:57

@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 this PR and didn't find any bugs this pass. The two nits from the previous round (ArrayPrototypeSplice capture, and rolling back the SNI entry on the H3 failure path / dropping the dead post-success error-queue checks in add_sni_context) are both addressed in f623106. Because this touches TLS/SNI plumbing end to end — SSL_CTX lifecycle across openssl.c/App.h (including the keep-alive-connection UAF guard in removeServerName), a new native binding into a running uWS app, and the shared server-TLS option pipeline — a human look would still be worthwhile.

What was reviewed:

  • add_sni_context error paths: probe-before-remove ordering, H3 rollback, no route-less SNI entry left registered on any exit.
  • App.h::removeServerName clearing the per-SNI ex_data slot so live keep-alive sockets fall back to the default router instead of the freed one.
  • serverTlsFromOptions extraction from _http_server.ts — verified the moved block is byte-equivalent modulo the alwaysTls gate; normalizeServerTls still applied on every path.
  • kSNIContexts dedup + the kRealListen tls-array assembly (default entry always first, primordial-safe splice/push).
Extended reasoning...

Overview

This PR makes https.Server a real subclass of http.Server and wires addContext/setSecureContext/get/setTicketKeys onto it, matching Node's tls.Server inheritance. It spans 14 files: the JS layer (https.ts, _http_server.ts, a new shared serverTlsFromOptions helper in internal/tls.ts, the kSNIContexts symbol, an empty-hostname guard on tls.Server#addContext), a new native binding (httpServerAddServerNameadd_sni_context in server_body.rs), the restored uws_remove_server_name FFI in App.rs, and a memory-safety change in the C/C++ usockets/uWS layer (us_internal_ssl_ctx_clear_sni_userdata + its call site in App.h::removeServerName). Fifteen new tests plus one added to node-tls-context.test.ts.

Security risks

The change is squarely in TLS territory: per-SNI SSL_CTX construction, client-certificate policy propagation (requestCert/rejectUnauthorized carried through normalizeServerTls and apply_client_cert_policy), and swapping SNI contexts on a running server. The App.h hunk is a UAF guard — a keep-alive connection accepted under a since-removed SNI entry would otherwise dereference the freed per-domain HttpRouter* via us_socket_server_name_userdata(); the fix nulls the ex_data slot so HttpContext falls back to the default router. That is the right shape and is covered by the "re-add does not break keep-alive connections" test, but it is exactly the kind of native lifetime change a maintainer should sign off on. I did not find a path where an https server's ca alone starts requiring client certificates (the normalizeServerTls defaults are preserved on every entry point), nor a way for a rejected setSecureContext to leave a half-applied config.

Level of scrutiny

High. This is not a mechanical change: it introduces a new native entry point that mutates a live uWS SSL app, restores a previously-removed FFI symbol, refactors the constructor's TLS option pipeline into a shared helper used by three call sites, and adds C code that manipulates BoringSSL ex_data on an SSL_CTX still referenced by open sockets. It has been through eleven review iterations here with every finding addressed, and the test coverage is thorough (pre-/post-listen, repeated hostnames, pfx, minVersion clearing, invalid-option atomicity, keep-alive across re-add), but the surface area and the native memory-safety component put it outside what an automated review should approve on its own.

Other factors

All prior inline findings from earlier passes are marked resolved with matching fix commits, and the current diff reflects the last two (primordial splice; add_sni_context no longer has post-success error exits that could strand a route-less SNI entry, and the H3 failure path now unregisters the TCP entry it just added). The _http_server.ts diff is a clean move of the option block into internal/tls.ts plus the new kSNIContexts fan-out at listen time. The two documented limitations (no SecureContext-object input, post-listen setSecureContext deferring to next listen()) match tls.Server's current behavior in Bun and are called out in the description.

robobun and others added 4 commits August 16, 2026 10:43
https.Server was a direct alias for http.Server, so none of the
tls.Server prototype methods (addContext, setSecureContext,
getTicketKeys, setTicketKeys) were available on servers created via
https.createServer(). In Node.js https.Server extends tls.Server which
provides these.

Make https.Server its own class extending http.Server (the ALPN
defaulting that https.createServer did moves into the constructor, so
new https.Server() gets it too) and implement addContext by buffering
SNI contexts and passing them to Bun.serve as the tls array on listen;
when called after listen, a new native binding (httpServerAddServerName)
registers the SNI context on the running uWS SSL app, with the same
per-serverName client-certificate policy the listen-time entries get.
Repeated addContext for the same hostname replaces the previous context
(last wins) instead of throwing, and the new SSL_CTX is validated before
the old entry is removed. uWS removeServerName now clears the per-domain
SNI userdata so keep-alive connections fall back to the default router
rather than dereferencing the freed one.

setSecureContext updates the default TLS config before listen and
applies the same requestCert/rejectUnauthorized normalization as
normalizeServerTls. getTicketKeys/setTicketKeys are stubbed the same
way they are on tls.Server.

Fixes #12157
Fixes #24474

Co-authored-by: Sam <sam.shridhar1950f@gmail.com>
…them

setSecureContext wrote each option into the live TLS config as it went,
so a later option failing validation left the server with a partially
updated config (for example a new cert paired with the previous key).
Validate everything first and replace the config in one step, the way
tls.Server does. Pin the malformed-PEM addContext assertion to the PEM
error and shorten the comments around the SNI registration sequence.
…xt and setSecureContext

addContext() and setSecureContext() validated their own short list of
options, so everything http.Server's constructor has since learned to
accept (pfx, minVersion/maxVersion, secureProtocol, ciphers) was silently
dropped: a pfx context registered with no certificate, and a version pin
given to setSecureContext() was ignored while one set in the constructor
survived it. Move that option handling into one internal/tls helper used
by the constructor and both methods. https.Server marks itself TLS before
the constructor runs, so it always gets a full config and no longer needs
a hand-made one; setSecureContext() rebuilds the config from the options
it was given and keeps only the server's requestCert/rejectUnauthorized,
as in Node.

addContext("") now throws ERR_TLS_REQUIRED_SERVER_NAME up front instead
of failing the later listen() (or, after listen, filing an entry on the
SNI tree root that nothing can match), and the native binding rejects an
empty name the same way Listener::add_server_name does.
tls.Server#addContext() gets the same up-front check; it used to accept
the empty name and fail later, at listen() or in the native binding.
After add_server_name_with_options() succeeded, add_sni_context() still
had two exits (the error-queue checks after the add and after domain())
that returned before set_routes(), leaving the hostname registered with
an empty router so every request for it was closed without a response.
Both checks were dead anyway: building the context clears the error
queue first and domain() makes no TLS calls. Drop them, and when the
HTTP/3 registration fails, remove the entry just added to the TCP app
so nothing is left half-registered.

Also use the captured Array.prototype.splice in addContext(), like the
push next to it.
@robobun
robobun force-pushed the farm/88a7a915/https-server-addcontext branch from f623106 to 2233ef2 Compare August 16, 2026 19:45

@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 this PR and the automated bug-hunting pass found no issues; the earlier findings from prior runs have all been addressed. Because it's a ~800-line change spanning TLS/SNI handling (security-sensitive), a new native binding, and SSL_CTX/router lifetime management in uWS C/C++ and Rust, a human look is still warranted.

What was reviewed:

  • add_sni_context error paths — no exit leaves a route-less SNI entry registered; H3 failure now rolls back the TCP entry.
  • removeServerName UAF fix in App.h — the ex_data slot is cleared so keep-alive connections fall back to the default router; covered by the keep-alive re-add test.
  • serverTlsFromOptions refactor — verified byte-for-byte equivalence with the constructor logic it replaces in _http_server.ts; setSecureContext validate-then-assign ordering.
  • Primordial captures (ArrayPrototypeSplice/Push) and the ERR_TLS_REQUIRED_SERVER_NAME guard applied to both https.Server and tls.Server siblings.
Extended reasoning...

Overview

This PR makes https.Server a distinct subclass of http.Server and wires addContext/setSecureContext/getTicketKeys/setTicketKeys to it (fixing #12157/#24474). It touches 14 files across four layers: built-in JS modules (src/js/node/https.ts, _http_server.ts, tls.ts, internal/tls.ts, internal/http.ts), a new Rust native binding (node_http_binding.rsserver_body.rs::add_sni_context), the uWS Rust FFI (App.rs restoring remove_server_name), and uWS/usockets C/C++ (App.h::removeServerName clearing SNI ex_data, openssl.c adding us_internal_ssl_ctx_clear_sni_userdata). The constructor's TLS-option validation is extracted into a shared serverTlsFromOptions helper so the constructor, addContext, and setSecureContext cannot drift. 15 new tests plus a sibling test in node-tls-context.test.ts.

Security risks

TLS/SNI is security-sensitive by nature. Specific surfaces I looked at: (1) the per-SNI client-certificate policy — add_sni_context passes apply_client_cert_policy = true matching the listen-time array path, and setSecureContext preserves the server's requestCert/rejectUnauthorized rather than letting a certificate swap silently drop mTLS enforcement (covered by a test); (2) empty-hostname rejection so an empty name never registers on the SNI tree root; (3) NUL-byte rejection before CString::new; (4) the UAF hazard in App.h::removeServerName where live keep-alive connections holding a ref on the removed SSL_CTX would otherwise dereference the freed per-domain router — the new us_internal_ssl_ctx_clear_sni_userdata clears the ex_data slot so those connections fall back to the default router. I did not find a way for user input to bypass validation or leave a half-registered SNI entry.

Level of scrutiny

High. This is not a mechanical change: it adds a new native FFI entry point, manipulates SSL_CTX refcounts and per-domain HttpRouter lifetimes in C++, and reorders remove/add on a running server. The refactor of _http_server.ts's TLS-option handling into internal/tls.ts is behavior-preserving on inspection but shifts ~100 lines of load-bearing validation. The keep-alive-across-re-add UAF fix in App.h is exactly the kind of change that benefits from a second pair of eyes on the ownership model (pendingServerNames is the single owner; listeners hold borrowed pointers; live SSL* objects hold their own SSL_CTX ref via SSL_set_SSL_CTX).

Other factors

The PR has been through 12 iterations with multiple prior review rounds; every earlier finding (dead error-queue checks after a successful add, ArrayPrototypeSplice primordial, unhandled-rejection in the keep-alive test, sibling tls.Server#addContext guard) has been addressed and all threads are resolved. Test coverage is thorough (pre/post-listen, repeated hostnames, invalid material rollback, pfx/minVersion, client-cert policy retention, keep-alive re-add). The known limitations (SecureContext objects not accepted, post-listen setSecureContext deferred to next listen()) are documented in the description and match tls.Server's current behavior. Given the breadth across native TLS plumbing, deferring to a human reviewer rather than auto-approving.

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.

Node's https server doesn't have addContext function [node compatibility] https.Server should inherit from tls.Server

1 participant