Skip to content

node:tls: report a wrapped stream's error on the TLS socket - #42240

Closed
robobun wants to merge 1 commit into
robobun/73a87144/tls-duplex-close-propagatesfrom
robobun/341cef9c/tls-wrapped-stream-error
Closed

robobun wants to merge 1 commit into
robobun/73a87144/tls-duplex-close-propagatesfrom
robobun/341cef9c/tls-wrapped-stream-error

Conversation

@robobun

@robobun robobun commented Sep 10, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #42176, which names this gap as its hand-off.

Problem

  • A TLS socket over a user stream (tls.connect({ socket: duplex }), new tls.TLSSocket(duplex, { isServer: true })) never hears that stream's 'error'. transport.listenerCount("error") is 0 after the wrap (node: 1). transport.destroy(err) becomes an uncaughtException, and https.request() over such a socket never settles.
  • Cause: attachUpgradedDuplex (src/js/node/net.ts:2303) wires only the engine's thunks. Node routes the stream's error: wrap.on('error', err => this._emitTLSError(err)).

Fix

  • attachUpgradedDuplex adds connection.on("error", err => self._emitTLSError(err)) when the transport is not a net.Socket. _emitTLSError already exists in tls.ts with node's routing: a client reports 'error', a server wrap keeps it on '_tlsError'.
  • Correct because it is node's listener at node's attach point. node:tls: keep unread data and still emit 'close' when a wrapped transport closes #42176's 'close' listener destroys the socket afterwards, so every case prints node v26.3.0's event list.
  • A net.Socket transport is unchanged. Its close paths report a peer reset only when listenerCount("error") > 0, so the listener made silent teardown resets uncaught (test-tls-inception.js). See Notes.
  • Verified: test/js/node/tls/node-tls-connect.test.ts ("hears that transport's error", 5 cases, all fail on the base branch and on main). Also test/js/node/tls/, 245 vendored test-tls-*/test-https-* files, and the tls suites on Windows.

Background

  • A stream with no fd cannot take the kernel TLS path. bun runs a BoringSSL engine over it (upgradeDuplexToTLS), fed by four native thunks on the stream's events.
  • _controlReleased is node's ownership flag. tls.connect() releases control at once, a server socket on 'secure'. Until then errors stay on '_tlsError', which a tls.Server turns into 'tlsClientError'.
Notes

node v26.3.0 vs this branch, same scripts

case bun 1.4.3 this branch node
tls.connect({ socket }), transport.destroy(err) same tick uncaught:EBOOM, socket never closes error:EBOOM, close:false same
same, mid-handshake uncaught:EBOOM error:EBOOM, close:false same
server wrap, same tick uncaught:EBOOM _tlsError:EBOOM, close:false same
server wrap, engine started uncaught:EBOOM _tlsError:EBOOM, close:false same
https.request over the wrap uncaught:EBOOM, req.destroyed === false for good req.error:EBOOM, req.close same
tlsSocket.destroy() over Duplex.from({ readable, writable }) uncaught ABORT_ERR error:ABORT_ERR, close:false same
transport whose write calls back with an error, s.write(data, cb) queued uncaught, cb never called error, cb(ERR_SOCKET_CLOSED), close:false error, cb(ECANCELED), close:false

The last row's callback code differs (node cancels the queued write through JSStreamSocket.doClose). #35386 covers ECANCELED for cancelled TLS writes.

Left out on purpose: every net.Socket transport

Two arms wrap a net.Socket, and neither gets the listener here:

  • The fd-adoption arm. tls.connect({ socket: connectedNetSocket }) with no queued writes hands the fd to a native TLS pair (upgradeTLS, net.ts around lines 2028 and 2426) and attaches nothing to the net.Socket.
  • The stream-level engine over a net.Socket: TLS over TLS, a named pipe, or a socket with queued plain writes. These go through attachUpgradedDuplex and are skipped by the instanceof Socket check.

Node attaches the same listener in both, so sock.destroy(err) on such a transport is still uncaught in bun. The reason is one mechanism. SocketEmitEndNT, SocketHandlers2.close, failWrite and ServerHandlers.error turn a peer reset into destroy(ECONNRESET) only when the socket has an 'error' listener, and stay silent otherwise. A forwarder counts as a listener. The first push of this PR attached it to every transport, and test-tls-inception.js (TLS over TLS, no 'error' listener anywhere) then failed on Windows x64, Windows arm64 and macOS arm64 with an uncaught read ECONNRESET: the proxy's teardown reset on the inner socket, silent before, reached the outer socket. On Windows the base branch passes 5 of 5 runs, the first push failed 5 of 5, and this head passes 6 of 6.

Covering these transports needs those four gates to look through the forwarder to the TLS socket's own listeners, and that changes what an HTTPS-proxy flow with an 'error' listener sees at teardown. That wants its own cross-platform validation. It overlaps #38122 (connect failure of a wrapped socket) and #36534 (tieToWrappedStream, which forwards with destroy(err) rather than _emitTLSError). Tracked as a follow-up.

Self-review

Concerns raised: conflicting helper with #42176 (stacked on it instead), server test loosened with slice(0, 2) (now exact lists), no same-tick destroy(err) case (added), wrong wrap.js citation (now L976-L977 and js_stream_socket.js L65), reach claim too broad (narrowed to user streams), fd-adoption arm uncovered (left out, above).

Related open PRs


[human-review] gate passed · iteration 0 · 2 files touched

fails on main (without fix)
ASAN without fix: 5 failed, 18 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/node/tls/node-tls-connect.test.ts
bun test v1.4.3 (5f554969b)

test/js/node/tls/node-tls-connect.test.ts:
(pass) should have checkServerIdentity [4.16ms]
(pass) should thow ECONNRESET if FIN is received before handshake [497.69ms]
(pass) initializes authorizationError to null in the TLSSocket constructor [12.13ms]
(pass) setMaxSendFragment mirrors OpenSSL's [512, 16384] acceptance without throwing [177.38ms]
(pass) should be able to grab the JSStreamSocket constructor [21.61ms]
(skip) tls.connect > should work with alpnProtocols
(pass) tls.connect > Bun.serve() should work with tls and Bun.file() [117.21ms]
(pass) tls.connect > should have peer certificate when using self asign certificate [141.93ms]
(skip) tls.connect > should have peer certificate
(skip) tls.connect > getCipher, getProtocol, getEphemeralKeyInfo, getSharedSigalgs, getSession, exportKeyingMaterial and isSessionReused should work
(skip) tls.connect > should process options correctly when connect is called with only options
(skip) tls.connect > should process port
... (truncated)

release without fix: 8 failed, 18 skipped
bun test v1.4.3-canary.1 (5f554969b)

test/js/node/tls/node-tls-connect.test.ts:
(pass) should have checkServerIdentity [0.03ms]
(pass) should thow ECONNRESET if FIN is received before handshake [6.75ms]
(pass) initializes authorizationError to null in the TLSSocket constructor [0.17ms]
(pass) setMaxSendFragment mirrors OpenSSL's [512, 16384] acceptance without throwing [3.33ms]
(pass) should be able to grab the JSStreamSocket constructor [0.20ms]
(skip) tls.connect > should work with alpnProtocols
(pass) tls.connect > Bun.serve() should work with tls and Bun.file() [6.48ms]
(pass) tls.connect > should have peer certificate when using self asign certificate [5.07ms]
(skip) tls.connect > should have peer certificate
(skip) tls.connect > getCipher, getProtocol, getEphemeralKeyInfo, getSharedSigalgs, getSession, exportKeyingMaterial and isSessionReused should work
(skip) tls.connect > should process options correctly when connect is called with only options
(skip) tls.connect > should process port and host correctly
(skip) tls.connect > should process port, host, and callback correctly
(skip) tls.connect > should handle the absence of a callback gracefully
(skip) tls.c
... (truncated)
passes on PR (with fix)
ASAN with fix: 18 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/node/tls/node-tls-connect.test.ts
bun test v1.4.3 (5f554969b)

test/js/node/tls/node-tls-connect.test.ts:
(pass) should have checkServerIdentity [2.83ms]
(pass) should thow ECONNRESET if FIN is received before handshake [315.79ms]
(pass) initializes authorizationError to null in the TLSSocket constructor [7.57ms]
(pass) setMaxSendFragment mirrors OpenSSL's [512, 16384] acceptance without throwing [110.82ms]
(pass) should be able to grab the JSStreamSocket constructor [12.40ms]
(skip) tls.connect > should work with alpnProtocols
(pass) tls.connect > Bun.serve() should work with tls and Bun.file() [76.24ms]
(pass) tls.connect > should have peer certificate when using self asign certificate [80.68ms]
(skip) tls.connect > should have peer certificate
(skip) tls.connect > getCipher, getProtocol, getEphemeralKeyInfo, getSharedSigalgs, getSession, exportKeyingMaterial and isSessionReused should work
(skip) tls.connect > should process options correctly when connect is called with only options
(skip) tls.connect > should process port an
... (truncated)

release with fix: 18 skipped
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 664ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/21] gen JS modules (bundle-modules)
Preprocess modules (8158ms)
Bundle modules (47ms)
Postprocesss modules (187ms)
Bundle Functions (513ms)
Generate Code (36ms)

[8.95s] Bundled "src/js" for production
  2594 kb
  197 internal modules
  13 native modules
  50 internal functions across 16 files
[1/7] cargo bun_runtime → libbun_runtime.a
�[1m�[92m   Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m   Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
�[1m�[92m   Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
�[1m�[92m   Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
�[1m�[92m   Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety)
�[1m�[92m   Compiling�[0m bun_base64 v0.0.0 (/workspace/bun/src/base64)
�[1m�[92m   Compiling�[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys)
�[1m�[92m   Compiling�[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys)
�[1m�[92m   Compiling�[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd)
�[1m�[92m   Compil
... (truncated)
diff hotspot
src/js/node/net.ts                        |  8 +++
 test/js/node/tls/node-tls-connect.test.ts | 94 +++++++++++++++++++++++++++++++
 2 files changed, 102 insertions(+)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                       reads  edits  tests
src/js/node/net.ts                            16      8     14
test/js/node/tls/node-tls-connect.test.ts      1      1     10

@robobun

robobun commented Sep 10, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review. Stacked on #42176, so the diff shows only the listener and its tests. Once #42176 lands this rebases onto main unchanged.

Reproduced on bun 1.4.3 and on main with the scripts in the Notes table: transport.listenerCount("error") is 0 after the wrap, and transport.destroy(err) reaches uncaughtException instead of the TLS socket. The five test cases fail against both the base branch and main src/, and pass with this change (debug + ASAN, linux x64).

The listener applies to user streams only. A net.Socket transport (TLS over TLS, a named pipe, queued plain writes) is unchanged, for the reason in the PR body's Notes.

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

LGTM — one listener wired at the same attach point as the existing 'close' forwarder, routed through the existing _emitTLSError.

What was reviewed:

  • Confirmed _emitTLSError already exists on TLSSocket.prototype (tls.ts:858) and is used elsewhere in net.ts, so the client/server routing is inherited, not reimplemented here.
  • The new line matches the sibling connection.on("close", ...) on the line above — same arrow style, same non-primordial .on on a user Duplex, consistent with the five other listeners in this helper.
  • Tests: added to the existing file, it.concurrent subprocess spawns with bunExe()/bunEnv/-e, Promise.all drain, combined-object toEqual on exact event lists, no network, no sleeps; the listeners:1 and uncaught: capture make the unfixed build fail on the event list rather than a timeout.
  • No CODEOWNERS coverage on the touched paths; the fd-adoption arm is explicitly scoped out with a stated reason.
Extended reasoning...

Overview

The runtime change is a single line in src/js/node/net.ts inside attachUpgradedDuplex: connection.on("error", err => self._emitTLSError(err)), plus a two-line why-comment and an added Node source-line citation. This helper already wires five other listeners (close, data, end, drain, close) from a wrapped Duplex transport onto the TLS socket that upgraded it; the new listener is the missing 'error' forwarder. _emitTLSError is an existing TLSSocket.prototype method (tls.ts:858) that already implements Node's routing ('_tlsError' while the server owns the socket, 'error' once control is released), and it is already called from net.ts:1110, so this adds no new mechanism. The rest of the diff is ~90 lines of tests in the existing test/js/node/tls/node-tls-connect.test.ts.

Security risks

None identified. The change is in the TLS module but touches only error-event propagation from an already-failed transport — no certificate validation, handshake logic, key material, or rejectUnauthorized handling is altered. The pre-fix behavior (an unhandled 'error' on the transport escalating to uncaughtException and leaving https.request hung forever) is strictly worse than the post-fix behavior. The listener does not swallow or downgrade any error; it routes it to the same channel Node uses.

Level of scrutiny

Low-to-moderate. The production delta is one line that mirrors the immediately preceding line's shape and calls an existing, already-used helper. The behavior is pinned to Node v26.3.0 with source-line citations to internal/tls/wrap.js L976-977 and internal/js_stream_socket.js L65. The variant matrix in the tests (client vs server wrap, three failure timings, and the https.request layer on top) covers the sibling paths REVIEW.md asks for, and the PR description explicitly names and justifies the one excluded arm (fd-adoption) as a follow-up. The bug hunter ran to dry_streak with no findings and no ruled-out candidates.

Other factors

Test conventions are followed closely: existing test file, bunExe()/bunEnv with -e, it.concurrent for independent subprocess cases, Promise.all to drain stdout/stderr/exited, a single toEqual on { events, stderr, exitCode }, no ports, no external network, no sleep/setTimeout waits (failure is triggered via queueMicrotask/setImmediate inside the child and observed via process.on("exit")). The listeners:1 assertion and the in-process uncaughtException recorder mean the unfixed build fails on the asserted event list (listeners:0 / uncaught:EBOOM) rather than by hanging. .github/CODEOWNERS covers neither changed path. No prior reviews or outstanding objections appear in the timeline.

A TLS socket that runs on the stream-level engine over a user stream
(tls.connect({ socket: duplex }), new tls.TLSSocket(duplex, { isServer: true }))
attached no 'error' listener to that stream. A transport failure became a
process-level uncaughtException and the TLS socket never learned about it:
https.request() over such a socket never settled.

attachUpgradedDuplex now forwards the stream's 'error' through
_emitTLSError, like node's JSStreamSocket: a client socket reports it as
'error', a server wrap keeps it on '_tlsError', which a tls.Server turns
into 'tlsClientError'.

A net.Socket transport (TLS over TLS, a named pipe, queued plain writes) is
left as it is. Its close paths report a peer reset only when the socket has
an 'error' listener, so the forwarder would surface teardown resets that are
silent today (test-tls-inception.js on Windows and macOS).
@robobun
robobun force-pushed the robobun/341cef9c/tls-wrapped-stream-error branch from 27884d4 to 6fcfb63 Compare September 10, 2026 23:34

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

Code review found no issues

No high-confidence issues detected in this change.

@robobun

robobun commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the review. One thing changed after the first review pass, so it is worth a second look.

The first push attached the 'error' listener to every transport. test-tls-inception.js (TLS over TLS, no 'error' listener anywhere) then failed on Windows and macOS with an uncaught read ECONNRESET. bun's net.Socket close paths report a peer reset only when the socket has an 'error' listener, and the forwarder counted as one. A teardown reset on the inner socket, silent before, reached the outer socket.

The current head (6fcfb63) attaches the listener only when the transport is not a net.Socket. User streams, which are the subject of this PR, are covered. net.Socket transports behave exactly as on the base branch. On Windows x64 the base passes test-tls-inception.js 5 of 5 runs, the first push failed 5 of 5, and the current head passes 6 of 6. The PR body's Notes describe what the net.Socket case needs and why it is a separate change.

The branch is also rebased onto the new head of #42176 (dc42eb4).

robobun added a commit that referenced this pull request Sep 12, 2026
…or fixture

Folds in the cases from #42240 that this branch did not have. The fixture
takes SIDE=client|server|https and also prints '_tlsError' and the 'close'
hadError flag. Every expected list is what node v26.3.0 prints.
@robobun

robobun commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator Author

Closing in favor of #42235, which carries the same fix: an 'error' listener on a TLS transport that is not a net.Socket, forwarded through _emitTLSError. #42235 was opened first and already has the test shape that its review asked for (bunRun, a fixture file, a short comment with the node links).

The cases that only this PR had are folded into #42235's fixture: a server wrap at both timings, https.request over the wrap, '_tlsError', and the 'close' hadError flag. The notes here about net.Socket transports and test-tls-inception.js on Windows and macOS are carried into #42235's body.

@robobun robobun closed this Sep 12, 2026
cirospaciari added a commit that referenced this pull request Sep 25, 2026
Builds on #42176, which is merged. The base of this PR is `main`.
Consolidates #42240 (the same fix).

### Problem

- A TLS upgrade over a generic `Duplex` listens for the transport's
`data`, `end`, `drain` and `close`, but not its `error`. So
`transport.destroy(err)` after the upgrade throws `err` as an uncaught
exception. Node emits it on the TLS socket.
- `transport.listenerCount('error')` is 0 in bun and 1 in node v26.3.0.
An `https.request` over such a socket kills the process and does not
fail the request.

### Fix

- `forwardUpgradedError` (`src/js/node/net.ts`) forwards the transport's
`error` through `_emitTLSError`. The four stream-engine attach sites
call it. A client sees `'error'`. A server wrap keeps it on
`'_tlsError'` (`'tlsClientError'` on a `tls.Server`). The transport's
`'close'` then ends the socket (#42176).
- A `net.Socket` transport (a named pipe, unflushed writes, TLS over
TLS) is excluded. Its close paths synthesize a read `ECONNRESET` as soon
as anything listens for `'error'`. See Notes.
- Verified: `test/js/node/tls/node-tls-connect.test.ts`, 5 new tests.
One runs `node-tls-duplex-transport-error-fixture.ts` through `bunRun`
(six wrap cases in one subprocess). All 5 fail on `main`. Also
`test/js/node/tls` and 9 vendored `test-tls-*` tests.

### Background

- A stream with no fd cannot use the kernel TLS path.
`upgradeDuplexToTLS` runs a BoringSSL engine over the stream, driven by
four native thunks on the stream's events. None is an error thunk.
- Node wraps the stream in a `JSStreamSocket`, which re-emits the
stream's `'error'`. `TLSSocket._init` forwards that error with
`_emitTLSError`.
- `_emitTLSError` already exists in `src/js/node/tls.ts`. It emits
`'_tlsError'` always, and `'error'` only once `_releaseControl` has run.
`tls.connect()` releases control at once, a server socket on `'secure'`.

<details><summary>Notes</summary>

#### Measurements

Each case wraps a `Duplex`, then calls `transport.destroy(new
Error("transport failed"))`. Events on the TLS socket (or the request),
linux x64, debug+ASAN. The first six rows are
`node-tls-duplex-transport-error-fixture.ts`, and the node column is
v26.3.0 with the same fixture. The `https.request` row runs in the test
process.

| case | `main` (has #42176) | this branch | node v26.3.0 |
| --- | --- | --- | --- |
| `tls.connect({ socket })`, destroy in the same tick | uncaught
`transport failed`, non-zero exit | `_tlsError`, `error`, `close:false`
| same |
| `tls.connect({ socket })`, destroy after the ClientHello | uncaught,
non-zero exit | `_tlsError`, `error`, `close:false` | same |
| server wrap, destroy in the same tick | uncaught, non-zero exit |
`_tlsError`, `close:false` | same |
| server wrap, destroy after the engine started | uncaught, non-zero
exit | `_tlsError`, `close:false` | same |
| `tlsServer.emit("connection", duplex)`, destroy in the same tick |
uncaught, non-zero exit | `tlsClientError` with the `TLSSocket`,
`close:false` | same |
| `tlsServer.emit("connection", duplex)`, destroy after the engine
started | uncaught, non-zero exit | `tlsClientError` with the
`TLSSocket`, `close:false` | same |
| `https.request` over the wrap | uncaught, non-zero exit | `req.error`,
`req.close` with `req.destroyed === true` | same |

A transport that errors without closing also matches node: the error
reaches the TLS socket and the socket stays alive (`destroyed ===
false`).

Two more cases from #42240, measured by hand on this branch (no test):

| case | bun 1.4.3-canary.1 | this branch | node v26.3.0 |
| --- | --- | --- | --- |
| `tlsSocket.destroy()` over `Duplex.from({ readable, writable })` |
uncaught `ABORT_ERR` | `error:ABORT_ERR`, `close:false` | same |
| transport whose `write` calls back with an error, `s.write(data, cb)`
queued | uncaught `EBOOM`, `cb` never called, no `'close'` |
`error:EBOOM`, `cb(ERR_SOCKET_CLOSED)`, `close:false` | `error:EBOOM`,
`cb(ECANCELED)`, `close:false` |

The callback code in the last row differs because node cancels the
queued write through `JSStreamSocket.doClose`. #35386 covers `ECANCELED`
for cancelled TLS writes.

#### Consolidation of #42240

#42240 carried the same listener with the same `net.Socket` guard. This
branch keeps the test shape the review here asked for (`bunRun`, a
fixture file, a one-line comment plus the node links). The cases that
only #42240 had are now in the fixture: a server wrap at both timings,
`https.request` over the wrap, `'_tlsError'`, and the `'close'`
`hadError` flag.

#42176 dropped the `attachUpgradedDuplex` helper that both PRs had
patched. It now attaches the four thunks inline at each site. So the
listener moved to `forwardUpgradedError`, which each site calls after it
attaches the four thunks.

#### Scope: every `net.Socket` transport is left out

Two arms wrap a `net.Socket`, and neither gets the listener:

- The fd-adoption arm. `tls.connect({ socket: connectedNetSocket })`
with no queued writes hands the fd to a native TLS pair (`upgradeTLS`)
and attaches nothing to the `net.Socket`.
- The stream-level engine over a `net.Socket`: TLS over TLS, a named
pipe, or a socket with queued plain writes. These reach
`forwardUpgradedError` and the `instanceof Socket` check skips them.

Node attaches the same listener in both, so `sock.destroy(err)` on such
a transport is still uncaught in bun. The reason is one mechanism.
`SocketEmitEndNT`, `SocketHandlers2.close`, `failWrite` and
`ServerHandlers.error` turn a peer reset into `destroy(ECONNRESET)` only
when the socket has an `'error'` listener, and stay silent otherwise. A
forwarder counts as a listener. The first push of #42240 attached it to
every transport. `test-tls-inception.js` (TLS over TLS, no `'error'`
listener anywhere) then failed on Windows x64, Windows arm64 and macOS
arm64 with an uncaught `read ECONNRESET`. On Windows x64 the base passed
5 of 5 runs, that push failed 5 of 5, and the narrowed listener passed 6
of 6.

Those arms also have a second route to the same failure (the raw
socket's own dispatch), so they need the de-duplication that #36534 and
#38122 are about.

`new tls.TLSSocket(stream)` with no `connect()` wraps nothing today, so
nothing reaches it there either. That is #37664's subject.
`_http2_upgrade.ts` has a fifth copy of the same four `.on()` calls.
#38124 routes its transport error to the h2 session.

#### Fixture startup

The fixture runs its six cases in one subprocess and takes the key and
the cert from `KEY` and `CERT`, which the test sets. An earlier shape
ran five subprocesses at once, and each imported `harness`. A debug
build needs about 1.5 s to load `node:tls` and about 1 s more for
`harness`, so on a loaded machine (load average 150 on 16 cores) two of
the five reached the 5 s test timeout. The one subprocess takes 2.1 to
2.8 s on the same machine.

#### Suites

`test/js/node/tls/node-tls-connect.test.ts`: 84 pass, 0 fail.

`test/js/node/tls/`: 391 pass, 3 fail, none on the Duplex wrap path.
`SNICallback runs even when the requested servername matches the bind
hostname` binds `localhost` and connects to `127.0.0.1`, and fails the
same way on the released binary in this container. `tls.Server socket
destroySoon > delivers the whole stream when destroySoon follows end`
(64 rounds of 2 MB) times out at 5 s on this debug build, and does the
same with `main`'s `net.ts`. `root certificate initialization >
concurrent Workers all see the same CA certificate lists` times out in
some runs on this loaded machine.

Vendored, all exit 0: `test-tls-inception`, `test-tls-js-stream`,
`test-tls-connect-given-socket`, `test-tls-delayed-attach-error`,
`test-tls-socket-failed-handshake-emits-error`,
`test-tls-over-http-tunnel`, `test-tls-starttls-server`,
`test-tls-destroy-stream`, `test-https-agent-create-connection`.

</details>

<!-- robobun:evidence:begin -->

---

**[human-review]** gate passed · iteration 0 · 3 files touched

<details><summary>fails on main (without fix)</summary>

```console
ASAN without fix: 7 failed, 18 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/node/tls/node-tls-connect.test.ts
bun test v1.4.3 (367d939)

test/js/node/tls/node-tls-connect.test.ts:
(pass) should have checkServerIdentity [4.73ms]
(pass) should thow ECONNRESET if FIN is received before handshake [534.04ms]
(pass) initializes authorizationError to null in the TLSSocket constructor [6.87ms]
(pass) setMaxSendFragment mirrors OpenSSL's [512, 16384] acceptance without throwing [120.38ms]
(pass) should be able to grab the JSStreamSocket constructor [14.03ms]
(skip) tls.connect > should work with alpnProtocols
(pass) tls.connect > Bun.serve() should work with tls and Bun.file() [116.64ms]
(pass) tls.connect > should have peer certificate when using self asign certificate [150.83ms]
(skip) tls.connect > should have peer certificate
(skip) tls.connect > getCipher, getProtocol, getEphemeralKeyInfo, getSharedSigalgs, getSession, exportKeyingMaterial and isSessionReused should work
(skip) tls.connect > should process options correctly when connect is called with only options
(skip) tls.connect > should process port 
... (truncated)

release without fix: 11 failed, 18 skipped
bun test v1.4.3-canary.1 (367d939)

test/js/node/tls/node-tls-connect.test.ts:
(pass) should have checkServerIdentity [0.03ms]
(pass) should thow ECONNRESET if FIN is received before handshake [6.17ms]
(pass) initializes authorizationError to null in the TLSSocket constructor [0.16ms]
(pass) setMaxSendFragment mirrors OpenSSL's [512, 16384] acceptance without throwing [3.30ms]
(pass) should be able to grab the JSStreamSocket constructor [0.21ms]
(skip) tls.connect > should work with alpnProtocols
(pass) tls.connect > Bun.serve() should work with tls and Bun.file() [5.48ms]
(pass) tls.connect > should have peer certificate when using self asign certificate [4.83ms]
(skip) tls.connect > should have peer certificate
(skip) tls.connect > getCipher, getProtocol, getEphemeralKeyInfo, getSharedSigalgs, getSession, exportKeyingMaterial and isSessionReused should work
(skip) tls.connect > should process options correctly when connect is called with only options
(skip) tls.connect > should process port and host correctly
(skip) tls.connect > should process port, host, and callback correctly
(skip) tls.connect > should handle the absence of a callback gracefully
(skip) tls.c
... (truncated)
```

</details>

<details><summary>passes on PR (with fix)</summary>

```console
ASAN with fix: 18 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/node/tls/node-tls-connect.test.ts
bun test v1.4.3 (367d939)

test/js/node/tls/node-tls-connect.test.ts:
(pass) should have checkServerIdentity [3.79ms]
(pass) should thow ECONNRESET if FIN is received before handshake [386.28ms]
(pass) initializes authorizationError to null in the TLSSocket constructor [6.71ms]
(pass) setMaxSendFragment mirrors OpenSSL's [512, 16384] acceptance without throwing [102.58ms]
(pass) should be able to grab the JSStreamSocket constructor [15.93ms]
(skip) tls.connect > should work with alpnProtocols
(pass) tls.connect > Bun.serve() should work with tls and Bun.file() [97.47ms]
(pass) tls.connect > should have peer certificate when using self asign certificate [122.29ms]
(skip) tls.connect > should have peer certificate
(skip) tls.connect > getCipher, getProtocol, getEphemeralKeyInfo, getSharedSigalgs, getSession, exportKeyingMaterial and isSessionReused should work
(skip) tls.connect > should process options correctly when connect is called with only options
(skip) tls.connect > should process port a
... (truncated)

release with fix: 18 skipped
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped)
  target       linux-x64-gnu
  build type   Release
  build dir    ./build/release
  revision     9098c05
  features     lto, baseline

23 deps, 136 codegen, 1176 objects in 977ms

ninja: Entering directory `/workspace/bun/build/release'
[1/4] fetch lolhtml
[lolhtml] up to date
[2/4] fetch rust-argon2
[rust-argon2] up to date
[2/4] cargo plan → /workspace/bun/build/release/rust-target/plan.json
244 units: 172 lib, 16 proc-macro (host), 19 custom-build (host), 15 run custom-build, 17 lib (host), 4 run custom-build (host), 1 rlib
[3/4] reconfigure
[1/1499] mkdir codegen
[2/1499] mkdir stamps
[3/1499] install /workspace/bun
bun install v1.4.3-canary.1 (367d939)

Checked 26 installs across 65 packages (no changes) [15.00ms]
[4/1499] install /workspace/bun/packages/bun-error
bun install v1.4.3-canary.1 (367d939)

Checked 1 install across 2 packages (no changes) [4.00ms]
[5/1499] install /workspace/bun/src/node-fallbacks
bun install v1.4.3-canary.1 (367d939)

Checked 111 installs across 104 packages (no changes) [6.00ms]
[6/1499] rustc unicode_ident 
[7/1499] gen node-fallbacks
... (truncated)
```

</details>

<details><summary>diff hotspot</summary>

```
src/js/node/net.ts                                 | 10 ++++
 test/js/node/tls/node-tls-connect.test.ts          | 45 ++++++++++++++++-
 .../tls/node-tls-duplex-transport-error-fixture.ts | 58 ++++++++++++++++++++++
 3 files changed, 112 insertions(+), 1 deletion(-)
```

</details>

**gate history** · 5 passed · 0 rejected · iteration 0

<details><summary>evidence per changed file</summary>

```
file                                                      reads  edits  tests
src/js/node/net.ts                                           13     12     41
test/js/node/tls/node-tls-connect.test.ts                     3      4     39
…/js/node/tls/node-tls-duplex-transport-error-fixture.ts      1      3     44
```

</details>

<!-- robobun:evidence:end -->

---------

Co-authored-by: robobun <robobun@bun.sh>
Co-authored-by: Ciro Spaciari <ciro.spaciari@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant