Skip to content

fetch: report OpenSSL reason (ERR_SSL_*) for non-certificate TLS handshake failures - #34921

Open
robobun wants to merge 3 commits into
mainfrom
farm/5a73bb98/fetch-tls-handshake-error-identity
Open

fetch: report OpenSSL reason (ERR_SSL_*) for non-certificate TLS handshake failures#34921
robobun wants to merge 3 commits into
mainfrom
farm/5a73bb98/fetch-tls-handshake-error-identity

Conversation

@robobun

@robobun robobun commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Non-certificate TLS handshake failures in fetch() were being reported as UNKNOWN_CERTIFICATE_VERIFICATION_ERROR, even with tls: { rejectUnauthorized: false } where a certificate error is impossible by construction. This sends operators hunting through CA stores when the actual fault is an ALPN mismatch, a non-TLS peer behind an https:// URL, or a mid-handshake connection close. Node reports the same scenarios with the OpenSSL reason (ERR_SSL_WRONG_VERSION_NUMBER, ERR_SSL_TLSV1_ALERT_NO_APPLICATION_PROTOCOL, ...).

Reproduction

import tls from "node:tls"; import net from "node:net";
const alpn = tls.createServer({ cert, key, ALPNProtocols: ["h2"] }, () => {});
alpn.on("tlsClientError", () => {});
const plain = net.createServer(s => s.end("HTTP/1.1 400 Bad Request\r\n\r\n"));
// ... listen both on port 0 ...
for (const [name, url] of [["alpn-mismatch", `https://127.0.0.1:${pa}/`],
                           ["plaintext-server", `https://127.0.0.1:${pb}/`]]) {
  try { await fetch(url, { tls: { rejectUnauthorized: false } }); }
  catch (e) { console.log(name, "->", e.code, "|", e.message); }
}

Before:

alpn-mismatch -> UNKNOWN_CERTIFICATE_VERIFICATION_ERROR | unknown certificate verification error
plaintext-server -> UNKNOWN_CERTIFICATE_VERIFICATION_ERROR | unknown certificate verification error

After:

alpn-mismatch -> ERR_SSL_TLSV1_ALERT_NO_APPLICATION_PROTOCOL | error:10000460:SSL routines:OPENSSL_internal:TLSV1_ALERT_NO_APPLICATION_PROTOCOL
plaintext-server -> ERR_SSL_WRONG_VERSION_NUMBER | error:100000f7:SSL routines:OPENSSL_internal:WRONG_VERSION_NUMBER

Cause

uSockets already dispatches these failures with a non-X509 sentinel: ssl_dispatch_parked_reason in packages/bun-usockets/src/crypto/openssl.c sends {error_no: -71, code: "EPROTO", reason: ERR_error_string_n(...)} for a fatal protocol error, and ssl_trigger_handshake_econnreset sends {error_no: -46, code: "ECONNRESET"} for a mid-handshake close. The HTTP client's handshake-failure branch in HTTPContext::on_handshake and ProxyTunnel::on_handshake fed the negative error_no into get_cert_error_from_no, whose X509_V_ERR_* lookup table maps any unknown value to UNKNOWN_CERTIFICATE_VERIFICATION_ERROR.

The proxy-tunnel inner-TLS path had two additional defects: SSLWrapper::update_handshake_state cleared the BoringSSL error queue before the handshake callback fired, discarding the reason; and ProxyTunnel::on_handshake only set did_have_handshaking_error inside its success branch, so a failed inner handshake always fell through to ConnectionRefused.

Fix

  • src/http/HTTPCertError.rs: new TLSHandshakeError struct that captures an owned copy of the sentinel's code/reason (the EPROTO reason is a stack buffer in uSockets, so an owned copy is required to outlive on_handshake), and node_error_code() which derives ERR_SSL_<REASON> from the last :-separated segment of the OpenSSL error string, matching Node's ThrowCryptoError.
  • src/http/HTTPContext.rs, src/http/ProxyTunnel.rs: in the handshake-failure branch, a negative error_no (X509_V_ERR_* values are all non-negative) records a TLSHandshakeError on the client state and fails with the new Error::TLSHandshakeFailed instead of routing through the X509 lookup. ProxyTunnel::on_handshake now hoists did_have_handshaking_error before branching on success, matching HTTPContext.
  • src/uws/lib.rs: SSLWrapper::update_handshake_state peeks ERR_peek_last_error() before clearing the queue on SSL_ERROR_SSL and dispatches the same -71/EPROTO shape uSockets does, with reason from BoringSSL's static ERR_reason_error_string table. Mirrors ssl_park_fatal_reason/ssl_dispatch_parked_reason in openssl.c.
  • src/http/{error.rs, InternalState.rs, lib.rs}: add the TLSHandshakeFailed variant and plumb tls_handshake_error through InternalState and HTTPClientResult (same side-channel shape as dns_hostname).
  • src/runtime/webcore/fetch/FetchTasklet.rs: when fail == TLSHandshakeFailed, report code = node_error_code() and message = reason.

Testing

New test in test/js/web/fetch/fetch.tls.test.ts stands up three hostile peers (a TLS server offering only an ALPN protocol fetch never advertises, a plain TCP server behind https://, and a CONNECT proxy that feeds garbage into the tunnel) and asserts each rejects with code: /^ERR_SSL_/ and a message carrying the OpenSSL reason; none may surface a certificate verification error. Fails on the unfixed build with exactly the UNKNOWN_CERTIFICATE_VERIFICATION_ERROR / ConnectionRefused mislabels above.

Supersedes #34182 and subsumes #31950: those map the sentinel to a generic ERR_TLS_HANDSHAKE_FAILED or EPROTO code without the OpenSSL reason. This PR carries the reason through to a Node-compatible ERR_SSL_* code.


[review] gate passed · iteration 0 · 9 files touched

fails on main (without fix)
ASAN without fix: 1 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/web/fetch/fetch.tls.test.ts
bun test v1.4.0 (a944c97c9)

test/js/web/fetch/fetch.tls.test.ts:
(pass) fetch-tls > fetch with valid tls and non-native checkServerIdentity that throws should reject [1137.39ms]
(pass) fetch-tls > fetch with valid tls should not throw [1730.26ms]
(pass) fetch-tls > can handle multiple requests with non native checkServerIdentity [1856.31ms]
(pass) fetch-tls > fetch with rejectUnauthorized: false should not call checkServerIdentity [272.87ms]
(pass) fetch-tls > re-derives the Host header and TLS verification hostname from the redirect target on a cross-origin redirect [2043.65ms]
(pass) fetch-tls > fetch with valid tls and non-native checkServerIdentity should work [1972.84ms]
(pass) fetch-tls > fetch with self-sign tls should throw [85.14ms]
(pass) fetch-tls > fetch with invalid tls should throw [78.95ms]
(pass) fetch-tls > fetch with checkServerIdentity failing should throw [143.29ms]
(pass) fetch-tls > fetch with checkServerIdentity rejects when connection closes before response headers [637.00ms]
... (truncated)

release without fix: all passed
bun test v1.4.0-canary.1 (b2b8aa2bc)

test/js/web/fetch/fetch.tls.test.ts:
(pass) fetch-tls > fetch with valid tls should not throw [1532.43ms]
(pass) fetch-tls > re-derives the Host header and TLS verification hostname from the redirect target on a cross-origin redirect [1552.12ms]
(pass) fetch-tls > checkServerIdentity approval still transmits the request and round-trips the response [37.86ms]
(pass) fetch-tls > fetch with checkServerIdentity rejects when connection closes before response headers [45.78ms]
(pass) fetch-tls > fetch with valid tls and non-native checkServerIdentity should work [1557.67ms]
(pass) fetch-tls > fetch with checkServerIdentity rejects when connection closes before response headers (with AbortSignal) [49.14ms]
(pass) fetch-tls > fetch should use NODE_EXTRA_CA_CERTS [41.83ms]
(pass) fetch-tls > checkServerIdentity rejection prevents the request from being transmitted [47.97ms]
(pass) fetch-tls > fetch with invalid tls + rejectUnauthorized: false should not throw [45.87ms]
(pass) fetch-tls > fetch with self-sign certificate tls + rejectUnauthorized: false should not throw [46.63ms]
(pass) fetch-tls > fetch should respect rejectUnauthorized e
... (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/web/fetch/fetch.tls.test.ts
bun test v1.4.0 (a944c97c9)

test/js/web/fetch/fetch.tls.test.ts:
(pass) fetch-tls > fetch with valid tls and non-native checkServerIdentity that throws should reject [1139.16ms]
(pass) fetch-tls > fetch with valid tls should not throw [1739.70ms]
(pass) fetch-tls > fetch with rejectUnauthorized: false should not call checkServerIdentity [113.32ms]
(pass) fetch-tls > re-derives the Host header and TLS verification hostname from the redirect target on a cross-origin redirect [2058.75ms]
(pass) fetch-tls > fetch with valid tls and non-native checkServerIdentity should work [1971.53ms]
(pass) fetch-tls > can handle multiple requests with non native checkServerIdentity [2056.23ms]
(pass) fetch-tls > fetch with self-sign tls should throw [151.97ms]
(pass) fetch-tls > fetch with invalid tls should throw [145.73ms]
(pass) fetch-tls > fetch with checkServerIdentity failing should throw [126.52ms]
(pass) fetch-tls > fetch with checkServerIdentity rejects when connection closes before response headers [602.50m
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 806ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/5] gen generated_host_exports.rs
generated_host_exports.rs: 91 exports (host=3, lazy=10, generic=78, rust=0); 244 extern-C blocks audited
[1/5] 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    Blocking�[0m waiting for file lock on build directory
�[1m�[92m   Compiling�[0m bun_uws v0.0.0 (/workspace/bun/src/uws)
�[1m�[92m   Compiling�[0m bun_event_loop v0.0.0 (/workspace/bun/src/event_loop)
�[1m�[92m   Compiling�[0m bun_bundler v0.0.0 (/workspace/bun/src/bundler)
�[1m�[92m   Compiling�[0m bun_http v0.0.0 (/workspace/bun/src/http)
�[1m�[92m   Compiling�[0m bun_spawn v0.0.0 (/workspace/bun/src/spawn)
�[1m�[92m   Compiling�[0m bun_patch v0.0.0 (/workspace/bun/src/patch)
�[1m�[92m   Compiling�[0m bun_standalone_graph v0.0.0 (/workspace/bun/src/standalone_graph)
�[1m�[92m   Compiling�[0m bun_transpiler v0.0.0 (/workspace/bun/src/transpiler)
�[1m�[92m   Compiling�[0m bun_bunfi
... (truncated)
diff hotspot
src/http/HTTPCertError.rs                 |  60 +++++++++++++++++
 src/http/HTTPContext.rs                   |  16 +++--
 src/http/InternalState.rs                 |   5 ++
 src/http/ProxyTunnel.rs                   |  18 +++--
 src/http/error.rs                         |   3 +
 src/http/lib.rs                           |  15 ++++-
 src/runtime/webcore/fetch/FetchTasklet.rs |  25 +++++++
 src/uws/lib.rs                            |  39 ++++++++---
 test/js/web/fetch/fetch.tls.test.ts       | 106 ++++++++++++++++++++++++++++++
 9 files changed, 269 insertions(+), 18 deletions(-)

gate history · 3 passed · 0 rejected · iteration 0

evidence per changed file
file                                       reads  edits  tests
src/http/HTTPCertError.rs                      2      4      0
src/http/HTTPContext.rs                        1      1      0
src/http/InternalState.rs                      2      2      0
src/http/ProxyTunnel.rs                        2      2      0
src/http/error.rs                              3      2      0
src/http/lib.rs                                7      8      0
src/runtime/webcore/fetch/FetchTasklet.rs      3      1      0
src/uws/lib.rs                                 2      3      0
test/js/web/fetch/fetch.tls.test.ts            2      3      0

When fetch()'s TLS handshake fails for a reason unrelated to certificate
verification (server sends a fatal ALPN alert, peer is not speaking TLS,
or the socket closes mid-handshake), the error was reported as
UNKNOWN_CERTIFICATE_VERIFICATION_ERROR even with rejectUnauthorized:false,
where a certificate error is impossible by construction. Node reports the
same scenarios as ERR_SSL_WRONG_VERSION_NUMBER / the alert identity.

uSockets already dispatches these as {error_no: -71, code: "EPROTO",
reason: <openssl error string>} (ssl_dispatch_parked_reason) or
{error_no: -46, code: "ECONNRESET"} (ssl_trigger_handshake_econnreset);
the HTTP client was feeding the negative error_no into
get_cert_error_from_no(), whose X509_V_ERR_* table sent it to the
catch-all UNKNOWN_CERTIFICATE_VERIFICATION_ERROR.

Fix: the handshake-failure branch in HTTPContext::on_handshake and
ProxyTunnel::on_handshake now captures the sentinel's code and reason
into an owned TLSHandshakeError on the client result (same side-channel
shape as dns_hostname) and fails with Error::TLSHandshakeFailed;
FetchTasklet::on_reject derives Node's ERR_SSL_<REASON> code from the
OpenSSL reason string. SSLWrapper::update_handshake_state is taught to
peek ERR_peek_last_error before clearing the queue so the proxy-tunnel
inner-TLS path carries the same identity. ProxyTunnel::on_handshake also
hoists did_have_handshaking_error before the success branch so a failed
inner handshake no longer falls through to ConnectionRefused.
@robobun

robobun commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 7:05 AM PT - Jul 21st, 2026

@robobun, your commit a944c97c9898ae90258f8f1fa822e65957b6d874 passed in Build #76832! 🎉


🧪   To try this PR locally:

bunx bun-pr 34921

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

bun-34921 --bun

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. fetch: report TLS protocol errors as ERR_TLS_HANDSHAKE_FAILED instead of ConnectionRefused / unknown-cert #34182 - Same scope: fixing TLS handshake errors in fetch() being misreported. PR fetch: report OpenSSL reason (ERR_SSL_*) for non-certificate TLS handshake failures #34921 explicitly supersedes this PR. Both modify HTTPContext.rs, ProxyTunnel.rs, error.rs, FetchTasklet.rs, and uws/lib.rs.
  2. Report connection resets during the TLS handshake as ECONNRESET instead of a certificate error #31950 - Fixes connection resets during TLS handshake being incorrectly reported as certificate errors. PR fetch: report OpenSSL reason (ERR_SSL_*) for non-certificate TLS handshake failures #34921 explicitly subsumes this PR. Both modify lib.rs and FetchTasklet.rs.

🤖 Generated with Claude Code

@robobun

robobun commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author

Intentional. As noted in the PR body this supersedes #34182 and subsumes #31950: those map the uSockets handshake-failure sentinel to a generic ERR_TLS_HANDSHAKE_FAILED / EPROTO code; this PR carries the OpenSSL reason through to Node's ERR_SSL_<REASON> code (ERR_SSL_WRONG_VERSION_NUMBER, ERR_SSL_TLSV1_ALERT_NO_APPLICATION_PROTOCOL, ...) and keeps the reason string as the error message. The SSLWrapper peek-before-clear and the ProxyTunnel did_have_handshaking_error hoist from #34182 are incorporated.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

The HTTP client now captures non-certificate TLS handshake reasons from OpenSSL, propagates them through client results, and maps them to structured fetch errors. Tests cover ALPN failures, plaintext HTTPS responses, and invalid proxy tunnel data.

TLS handshake failure reporting

Layer / File(s) Summary
TLS error contract
src/http/HTTPCertError.rs, src/http/error.rs, src/http/InternalState.rs
Adds TLSHandshakeError, TLSHandshakeFailed, and internal state storage for handshake details.
Handshake reason capture
src/uws/lib.rs, src/http/HTTPContext.rs, src/http/ProxyTunnel.rs
Captures OpenSSL reasons and separates negative handshake sentinels from certificate verification errors.
HTTP result propagation
src/http/lib.rs
Carries handshake errors through HTTP/1.1, h2/h3, lifetime detachment, and final results.
Fetch error mapping and validation
src/runtime/webcore/fetch/FetchTasklet.rs, test/js/web/fetch/fetch.tls.test.ts
Maps handshake details to Node-style system errors and validates representative protocol failures.

Possibly related PRs

  • oven-sh/bun#34182: Implements a closely related TLS protocol-failure reporting path through HTTP, fetch, and uSockets.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: reporting OpenSSL ERR_SSL_* reasons for non-certificate TLS handshake failures.
Description check ✅ Passed The description covers the PR purpose, root cause, fix, and verification, though it does not use the template's exact headings.

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

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Beyond the inline nit, I audited the other SSLWrapper::update_handshake_state consumers for the new -71/EPROTO dispatch shape: UpgradedDuplex/WindowsNamedPipe route to TLSSocket::on_handshake which already handles negative error_no (owned code_bytes()/reason_bytes() copies, same as the direct openssl.c path), and WebSocketProxyTunnel::on_handshake gates on error_no != 0 which is unchanged. Also confirmed ERR_reason_error_string returning NULL is handled — ZStr::from_c_ptr and reason_bytes() both map null to empty, and node_error_code() falls back to EPROTO.

Extended reasoning...

The SSLWrapper change in src/uws/lib.rs is a shared helper touched by four handshake consumers (ProxyTunnel, UpgradedDuplex, WindowsNamedPipe, WebSocketProxyTunnel). I traced each to confirm the new {error_no: -71, code: "EPROTO", reason: ERR_reason_error_string(...)} shape is compatible: TLSSocket::on_handshake (socket_body.rs:1642, reached via UpgradedDuplex/WindowsNamedPipe) already boxes code/reason as owned bytes and was designed for this shape from the direct openssl.c path; WebSocketProxyTunnel gates on error_no != 0. I also checked the null-reason edge: ERR_reason_error_string can return NULL, but ZStr::from_c_ptr (bun_core/util.rs:236) and us_bun_verify_error_t::reason_bytes both map null to empty, and node_error_code() falls back to the sentinel code. Deferring to human review given this touches TLS handshake error handling across the HTTP client, proxy tunnel, and shared SSLWrapper.

Comment thread test/js/web/fetch/fetch.tls.test.ts Outdated

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

Beyond the inline nit, I also checked: ERR_reason_error_string returning NULL in the SSLWrapper path — reason_bytes() maps null to b"", and node_error_code()/FetchTasklet fall back to EPROTO / "TLS handshake failed"; and InternalState::reset() — the new tls_handshake_error field is cleared via ..Default::default() on redirect/retry.

Extended reasoning...

This run's inline finding is a code-style nit (magic 8 vs b"ERR_SSL_".len()). While reviewing the Rust side I traced the null-pointer path for the SSLWrapper's ERR_reason_error_string result and the redirect/retry reset of the new state field; both are handled. The PR is otherwise a well-scoped error-reporting fix, but it touches TLS handshake failure control flow across HTTPContext/ProxyTunnel/SSLWrapper (including the did_have_handshaking_error hoist in ProxyTunnel), so I'm leaving it for human review rather than approving.

Comment thread src/http/HTTPCertError.rs Outdated

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

No issues found, but this touches TLS handshake error handling across the HTTP client, ProxyTunnel, and SSLWrapper (BoringSSL FFI), plus a behavior change hoisting did_have_handshaking_error in ProxyTunnel::on_handshake — worth a human look.

What was reviewed:

  • error_no < 0 correctly discriminates the uSockets -71/-46 sentinels from non-negative X509_V_ERR_* codes; owned Box<[u8]> copy is required since ssl_dispatch_parked_reason's reason is a stack buffer.
  • ERR_reason_error_string returning NULL is handled (reason_bytes()b""node_error_code() falls back to EPROTO; FetchTasklet falls back to "TLS handshake failed").
  • tls_handshake_error plumbing through InternalState/HTTPClientResult/detach_lifetime mirrors the existing dns_hostname side-channel exactly.
  • Test covers both the direct uSockets path and the SSLWrapper inner-TLS path; both prior nits (try/finally ordering, ERR_SSL_ prefix length) addressed.
Extended reasoning...

Overview

This PR fixes fetch() misreporting non-certificate TLS handshake failures (ALPN mismatch, non-TLS peer behind https://, mid-handshake close) as UNKNOWN_CERTIFICATE_VERIFICATION_ERROR. It captures the OpenSSL reason from the uSockets handshake-failure sentinel and surfaces Node-compatible ERR_SSL_<REASON> codes. Touches 9 files: a new TLSHandshakeError struct in HTTPCertError.rs, a new Error::TLSHandshakeFailed variant, error_no < 0 branching in both HTTPContext::on_handshake and ProxyTunnel::on_handshake, ERR_peek_last_error-before-clear in SSLWrapper::update_handshake_state, plumbing through InternalState/HTTPClientResult/lib.rs, and mapping in FetchTasklet.rs. ~268 net lines including a 3-scenario test.

Security risks

Low. This only changes which error is reported when a TLS handshake fails — it does not touch the success path, certificate verification logic, rejectUnauthorized gating, or checkServerIdentity. A wrong error code here is a diagnostic issue, not an auth bypass. The ProxyTunnel hoist of did_have_handshaking_error before the handshake_success branch makes the failure branch actually reachable (previously fell through to ConnectionRefused), which is strictly more accurate reporting.

Level of scrutiny

Moderate. The change is error-path-only and well-tested, but it spans the HTTP client's TLS handshake handlers, the SSLWrapper BoringSSL state machine, and cross-thread result plumbing. The SSLWrapper change introduces new BoringSSL FFI calls (ERR_peek_last_error/ERR_reason_error_string) inside the laundered-self update_handshake_state path, and ProxyTunnel::on_handshake gains a state write (this.state.tls_handshake_error = ...) inside the aliasing-sensitive callback shape. I traced the NLL/aliasing contracts at both sites and they hold (the &mut HTTPClient borrow ends before close_from_callback), but this is exactly the kind of code where a maintainer familiar with the ProxyTunnel aliasing discipline should confirm.

Other factors

  • Both prior nits I raised (server .listen() inside try, deriving the ERR_SSL_ prefix length) were addressed in b2b8aa2 and a944c97.
  • The test exercises all three failure shapes (ALPN alert via direct uSockets path, non-TLS peer via direct path, garbage-in-tunnel via SSLWrapper path) and asserts specific ERR_SSL_* codes rather than just "not a cert error".
  • I checked that ERR_reason_error_string returning NULL degrades cleanly (reason_bytes() → empty → node_error_code() returns EPROTO, message falls back to the static string).
  • The tls_handshake_error field follows the exact same owned-side-channel pattern as dns_hostname through every HTTPClientResult construction/detach site in lib.rs.
  • The pre-existing HTTPCertError::from_verify_error still runs on the sentinel struct (widening a stack-buffer reason to &'static ZStr), but only .error_no is read from it in the new negative branch; the owned copy comes from the raw ssl_error — no new unsoundness introduced.

Deferring because 9 files across TLS handshake error handling + a ProxyTunnel behavior change is beyond "simple/mechanical", not because anything looks wrong.

@robobun

robobun commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author

CI on a944c97 (build 76832): 194/196 jobs passed. The new fetch.tls.test.ts coverage passed on every lane that ran it.

The two remaining darwin-14-aarch64-test-bun lanes never ran: one expired waiting for an agent and the other is still scheduled. Only a single darwin agent is currently connected to Buildkite (darwin-bagel-x64-1, x64), and no arm64 macOS 14 agents are available to pick up those jobs. The earlier darwin-26-aarch64 and darwin-14-x64 lanes ran and passed.

The five annotated failures (test-http-server-connections-checking-leak.js, napi.test.ts, in-process-cron.test.ts, jsonwebtoken/async_sign.test.js, bun-server.test.ts websocket-idle-CPU) are all marked flaky and passed on retry; none touch the HTTP client, proxy tunnel, or SSLWrapper paths this PR changes.

Ready for review; the only red is the darwin arm64 agent outage.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant