Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
209 commits
Select commit Hold shift + click to select a range
aaaa057
node:http/https/http2: raise Node v26.3.0 compat to ~93% and sync the…
cirospaciari Jun 22, 2026
a8d3064
node:http/https/http2: raise Node v26.3.0 compat to ~93% and sync the…
cirospaciari Jun 22, 2026
8d1e6a3
node:http: post-pad the request trailer section before the 8-byte fie…
cirospaciari Jun 26, 2026
e22f9d6
node:http: post-pad the request trailer section before the 8-byte fie…
cirospaciari Jun 26, 2026
aafedba
node:http: add a standalone server-timeout-knobs test
cirospaciari Jun 29, 2026
9ebcfa4
node:http: add a standalone server-timeout-knobs test
cirospaciari Jun 29, 2026
92050fb
test: keep the huge-ArrayBuffer structuredClone cases off the OOM killer
cirospaciari Jun 29, 2026
8ce10c8
test: keep the huge-ArrayBuffer structuredClone cases off the OOM killer
cirospaciari Jun 29, 2026
18c1c46
node:http2: restore a client stream's captured empty async context
cirospaciari Jun 29, 2026
bb4a012
node:http2: restore a client stream's captured empty async context
cirospaciari Jun 29, 2026
3946ff4
test: quarantine test-net-connect-memleak on linux-x64-musl
cirospaciari Jun 29, 2026
0e6f8b1
test: quarantine test-net-connect-memleak on linux-x64-musl
cirospaciari Jun 29, 2026
738780e
usockets: don't truncate a hung-up socket's stream at the poll's EOF …
cirospaciari Jun 29, 2026
73cf3b6
usockets: don't truncate a hung-up socket's stream at the poll's EOF …
cirospaciari Jun 29, 2026
c0ce5fe
node:http: do not shut the socket down while the response is still fl…
cirospaciari Jun 30, 2026
cd4aa05
node:http: do not shut the socket down while the response is still fl…
cirospaciari Jun 30, 2026
dd5400e
node:http/http2: validate h2 requests and harden server error paths
cirospaciari Jul 1, 2026
4f0e779
node:http/http2: satisfy the rust lints enforced in CI
cirospaciari Jul 1, 2026
e3090fd
node:http/http2: address review feedback and the src/js lint failure
cirospaciari Jul 1, 2026
e63fd76
node:http2: do not apply the request Host rules to client-received re…
cirospaciari Jul 1, 2026
b429076
node:http2: reject a malformed PUSH_PROMISE as a connection error, li…
cirospaciari Jul 1, 2026
62d8523
node:http2: give the push-promise unit test's promised request its ma…
cirospaciari Jul 1, 2026
3946946
Merge origin/main into claude/node-http-http2-compat
cirospaciari Jul 1, 2026
7aae0b8
Merge origin/main into claude/node-http-http2-compat
cirospaciari Jul 1, 2026
5ab67eb
Merge claude/node-http-http2-compat (origin/main sync) into claude/no…
cirospaciari Jul 1, 2026
aaaef71
Merge origin/main into claude/node-http-http2-compat
cirospaciari Jul 6, 2026
a370101
Merge origin/main into claude/node-http-http2-compat
cirospaciari Jul 6, 2026
6961bb8
node:http2: detach the allowHTTP1 fallback socket when a response fin…
cirospaciari Jul 6, 2026
27ac2f9
node:http2: detach the allowHTTP1 fallback socket when a response fin…
cirospaciari Jul 6, 2026
5e4ac63
Merge claude/node-http-http2-compat (origin/main sync) into claude/no…
cirospaciari Jul 6, 2026
8b4066c
node:http2: refuse over-budget streams with ENHANCE_YOUR_CALM like node
cirospaciari Jul 6, 2026
eabefc4
node:http2: refuse over-budget streams with ENHANCE_YOUR_CALM like node
cirospaciari Jul 6, 2026
02968cf
Merge branch 'job-8ad2a0dd/node-http-http2-compat' into job-8ad2a0dd/…
cirospaciari Jul 6, 2026
c2a68b3
node:http2: treat late frames on evicted locally-initiated streams as…
cirospaciari Jul 6, 2026
cd563f1
node:http2: treat late frames on evicted locally-initiated streams as…
cirospaciari Jul 6, 2026
20c07b3
Merge branch 'job-8ad2a0dd/node-http-http2-compat' into job-8ad2a0dd/…
cirospaciari Jul 6, 2026
d0ed19d
test: don't require an empty stderr from the throwing-handler http fi…
cirospaciari Jul 6, 2026
d43eb06
Merge origin/main into claude/node-http-http2-compat
cirospaciari Jul 7, 2026
13aaf3c
Merge origin/main into claude/node-http-http2-compat
cirospaciari Jul 7, 2026
8bf7e03
Merge branch 'job-8ad2a0dd/node-http-http2-compat' into job-8ad2a0dd/…
cirospaciari Jul 7, 2026
178c628
node:http: fix trailer/EOF parser hangs, leaks and Windows fatal-writ…
alii Jul 7, 2026
cb4f2dd
node:http: fix trailer/EOF parser hangs, leaks and Windows fatal-writ…
alii Jul 7, 2026
e9a8337
http/http2: address review — h2 response/push validation, TE trailing…
alii Jul 7, 2026
ab86b7d
Merge origin/claude/node-http-http2-compat into claude/node-http-http…
alii Jul 7, 2026
9129c09
node:http/http2: fix Node-compat divergences from review
alii Jul 7, 2026
b9b1ae1
node:http/http2: fix Node-compat divergences from review
alii Jul 7, 2026
05988e6
node:http/http2: dedup ERR_INVALID_ARG_TYPE formatter and clarify ser…
alii Jul 7, 2026
93063de
node:http/http2: dedup ERR_INVALID_ARG_TYPE formatter and clarify ser…
alii Jul 7, 2026
9c480b5
Merge claude/node-http-http2-compat into the http hardening branch
cirospaciari Jul 7, 2026
9d760d2
Merge remote-tracking branch 'origin/claude/node-http-http2-compat' i…
cirospaciari Jul 7, 2026
3763778
node:http: trailer/domain coverage + review-fix cleanups
alii Jul 7, 2026
9ab92d4
node:http: trailer/domain coverage + review-fix cleanups
alii Jul 7, 2026
3898852
node:http2: don't gate client close() on receiving the peer's SETTING…
alii Jul 7, 2026
5e249f0
node:http2: don't gate client close() on receiving the peer's SETTING…
alii Jul 7, 2026
c22eec8
node:http: honor server.httpAllowHalfOpen assigned after listen()
cirospaciari Jul 7, 2026
94255ac
node:http: honor server.httpAllowHalfOpen assigned after listen()
cirospaciari Jul 7, 2026
1355715
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 7, 2026
b7b7572
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 7, 2026
f4542e7
node:http: allow the Trailer response header on chunked responses
cirospaciari Jul 7, 2026
a390358
node:http: allow the Trailer response header on chunked responses
cirospaciari Jul 7, 2026
6944eca
node:http: capture request trailers per request, not per connection
cirospaciari Jul 7, 2026
4a8adab
node:http: capture request trailers per request, not per connection
cirospaciari Jul 7, 2026
4a3cf89
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 7, 2026
4d7f32f
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 7, 2026
c1d3957
node:http/http2: fix pipelined 1xx ordering, socket.parser at 'connec…
alii Jul 7, 2026
d7f2781
node:http/http2: fix pipelined 1xx ordering, socket.parser at 'connec…
alii Jul 7, 2026
8e7d70c
node:http/http2: fix pipelined 1xx ordering, socket.parser at 'connec…
alii Jul 7, 2026
b505f79
Merge origin/claude/node-http-http2-compat into claude/node-http-http…
alii Jul 8, 2026
c376074
node:http2: reopen receive window from _read() and clear kOwnsFd in r…
alii Jul 8, 2026
99eac9e
node:http2: reopen receive window from _read() and clear kOwnsFd in r…
alii Jul 8, 2026
61f0b45
node:http2: pair the receive-window reopen with Node's readStop-on-fu…
cirospaciari Jul 8, 2026
30392cc
node:http2: pair the receive-window reopen with Node's readStop-on-fu…
cirospaciari Jul 8, 2026
9c8dd2c
node:http: move node-compat per-connection state out of Bun.serve's H…
cirospaciari Jul 8, 2026
f2d6f54
node:http: move node-compat per-connection state out of Bun.serve's H…
cirospaciari Jul 8, 2026
15b1db2
node:http: compile the node-compat state and handlers out of Bun.serv…
cirospaciari Jul 8, 2026
b0433f5
node:http: compile the node-compat state and handlers out of Bun.serv…
cirospaciari Jul 8, 2026
ebfa237
node:http2: close a finished server stream whose request body was nev…
cirospaciari Jul 8, 2026
e12dfb7
node:http2: close a finished server stream whose request body was nev…
cirospaciari Jul 8, 2026
d2f54aa
node:http2: only auto-close an unread request when its body is actual…
cirospaciari Jul 8, 2026
bb67830
node:http2: only auto-close an unread request when its body is actual…
cirospaciari Jul 8, 2026
dc7d640
node:http2: make the unread-request close race-free; node:http fixes …
cirospaciari Jul 9, 2026
c14281e
node:http2: make the unread-request close race-free; node:http fixes …
cirospaciari Jul 9, 2026
b8e66e2
node:http2: run the settings() callback before emitting 'localSettings'
cirospaciari Jul 9, 2026
bd8fc38
node:http2: run the settings() callback before emitting 'localSettings'
cirospaciari Jul 9, 2026
4990051
node:http2: let a drained request finish before a graceful session de…
cirospaciari Jul 9, 2026
d93f3d9
node:http2: let a drained request finish before a graceful session de…
cirospaciari Jul 9, 2026
2194f30
node:http2: flush settings() and give its ACK a bounded grace on close
cirospaciari Jul 9, 2026
67f943d
node:http2: flush settings() and give its ACK a bounded grace on close
cirospaciari Jul 9, 2026
ac6d1ae
node:http: fail a request whose trailer section is malformed; honor i…
cirospaciari Jul 9, 2026
252aebb
node:http: fail a request whose trailer section is malformed; honor i…
cirospaciari Jul 9, 2026
a47f450
node:http: never dispatch a pipelined request behind Connection: close
cirospaciari Jul 9, 2026
23965a8
node:http: never dispatch a pipelined request behind Connection: close
cirospaciari Jul 9, 2026
d66bf87
Merge remote-tracking branch 'origin/main' into ciro/ci-70030-fix
cirospaciari Jul 9, 2026
46a59e0
Merge remote-tracking branch 'origin/main' into ciro/ci-70030-fix
cirospaciari Jul 9, 2026
8fffce0
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 9, 2026
7095981
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 9, 2026
302a333
document the trailer-validation capacity bound
cirospaciari Jul 9, 2026
7a2fe94
document the trailer-validation capacity bound
cirospaciari Jul 9, 2026
20bbfb0
node:http: server hot-path performance (stacked on #32488) (#33879)
cirospaciari Jul 10, 2026
269c81a
node:http: server hot-path performance (stacked on #32488) (#33879)
cirospaciari Jul 10, 2026
42ba97c
Merge origin/main into claude/node-http-http2-compat
cirospaciari Jul 10, 2026
d49cff1
Merge origin/main into claude/node-http-http2-compat
cirospaciari Jul 10, 2026
fdd1762
Merge branch 'main' into claude/node-http-http2-compat
cirospaciari Jul 10, 2026
5fb4a01
Merge branch 'main' into claude/node-http-http2-compat
cirospaciari Jul 10, 2026
0274b1c
Merge origin/claude/node-http-http2-compat into claude/node-http-http…
cirospaciari Jul 11, 2026
d1dbe3e
Merge branch 'main' into claude/node-http-http2-compat
cirospaciari Jul 11, 2026
74b1081
uws: fold the per-response booleans into the flags word, drop the nod…
cirospaciari Jul 11, 2026
cceac70
paths: return the resolved slice directly
cirospaciari Jul 11, 2026
c2ce18d
test: stop skipping the HTTP/2 test suite
Jarred-Sumner Jul 11, 2026
06e9db8
runtime: build the body-value pool on the heap instead of a stack tem…
cirospaciari Jul 11, 2026
3680876
Merge remote-tracking branch 'origin/main' into claude/node-http-http…
cirospaciari Jul 11, 2026
bc0b464
test: drop vendored http2 tests that depend on Node-internal machinery
cirospaciari Jul 11, 2026
d5ea586
uws: gate the leading-empty-line tolerance behind the node-http parser
cirospaciari Jul 11, 2026
0bd6587
node:http: drop the misleading optional chain on the pipelined queued…
cirospaciari Jul 11, 2026
9a53587
node:http2: ignore ECONNRESET once a GOAWAY has been received
cirospaciari Jul 11, 2026
031ab0b
node:http2: release the parser refs that process.exit() strands
cirospaciari Jul 11, 2026
072d922
worker: release the loop state a terminated worker leaves behind
cirospaciari Jul 13, 2026
79d630d
test: stop adding expectations.txt entries; gate the named-pipe tests…
cirospaciari Jul 13, 2026
dd56a89
uws: drop the last runtime node-http-compat plumbing and the maybe_un…
cirospaciari Jul 13, 2026
9dbfb99
node:http: bound pipelined response buffering; match Node on a missin…
cirospaciari Jul 13, 2026
deb66a0
Merge remote-tracking branch 'origin/main' into claude/node-http-http…
cirospaciari Jul 13, 2026
4d8585e
node:http: report a missing chunk terminator as HPE_STRICT, like llhttp
cirospaciari Jul 13, 2026
a61111d
Fix clippy let_and_return in the h2 write path
cirospaciari Jul 13, 2026
45480cf
node:https: deliver the TLS EOF to the HTTP layer instead of force-cl…
cirospaciari Jul 13, 2026
bd10ee6
node:http2: resume the socket on graceful session close, like Node
cirospaciari Jul 13, 2026
08e091b
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 13, 2026
116577c
node:http2: judge session idleness by a monotonic written counter, li…
cirospaciari Jul 13, 2026
4bee8a2
node:http2: tear the transport down when a native write fails fatally
cirospaciari Jul 13, 2026
adaa711
webcore: release the FileSink refs that process exit strands
cirospaciari Jul 13, 2026
8ef31c8
node:http: count header bytes in a queued pipelined response's output
cirospaciari Jul 13, 2026
d7bfa55
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 13, 2026
9c17084
node:http2: read session write progress from the native handle's counter
cirospaciari Jul 13, 2026
0d1e01e
node:http2: end the socket first and destroy a tick later on error te…
cirospaciari Jul 13, 2026
2d4a23f
test: calibrate the ASAN leak tests for sanitizer overhead; patch mim…
cirospaciari Jul 13, 2026
b4fa757
usockets: retry macOS's racy EPROTOTYPE like libuv instead of reporti…
cirospaciari Jul 13, 2026
8061d7f
usockets: don't resume a paused socket's reads when re-arming for a w…
cirospaciari Jul 13, 2026
07be335
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 13, 2026
f636e4a
node:http2: don't detach the JS wrapper when closing a fatally-failed…
cirospaciari Jul 13, 2026
5cecac8
usockets(windows): request UV_DISCONNECT so a peer reset fires with n…
cirospaciari Jul 13, 2026
e67ff52
node:http2: make session destroy idempotent, like Node
cirospaciari Jul 14, 2026
f61faf4
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 14, 2026
246c1dc
test: staged half-close-mid-upload teardown coverage
cirospaciari Jul 14, 2026
3807122
test: staged PING-flood teardown coverage
cirospaciari Jul 14, 2026
c59f9e9
usockets(windows): treat UV_DISCONNECT as the EOF hint and arm it unc…
cirospaciari Jul 14, 2026
8d03151
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 14, 2026
4b97a68
node:http2: don't latch destroy on the nothing-to-tear-down early-out
cirospaciari Jul 14, 2026
76c1a4b
node:http2: don't re-destroy an already-destroyed stream from the def…
cirospaciari Jul 14, 2026
2663f0f
node:http2: whitelist the errnos that fatally close the transport
cirospaciari Jul 14, 2026
929ac1e
usockets(windows): make UV_DISCONNECT one-shot so a deferred EOF cann…
cirospaciari Jul 14, 2026
55062db
node:http2: treat an error GOAWAY after our own graceful close as mut…
cirospaciari Jul 14, 2026
affdc52
test: pin the default-localhost loopback contract the vendored suites…
cirospaciari Jul 14, 2026
f74f866
usockets(windows): UV_DISCONNECT surfaces as readable only - recv() o…
cirospaciari Jul 14, 2026
326d048
test: give the abort fixtures measured release headroom
cirospaciari Jul 14, 2026
d5d0254
usockets(windows): map UV_DISCONNECT to EOF only for already-shut-dow…
cirospaciari Jul 14, 2026
29d1fe6
usockets(windows): sync poll_cb's header comment with the conditional…
cirospaciari Jul 14, 2026
4768dd3
test: extend the URLSearchParams leak deadline to ASAN like the debug…
cirospaciari Jul 14, 2026
b4b9de5
node:http2: only skip the deferred error sweep for listener-less dest…
cirospaciari Jul 14, 2026
8763fd9
test: event-taped twins for the two darwin-intermittent h2 scenarios
cirospaciari Jul 14, 2026
507986e
test: probe the PING-flood teardown on both loopback families
cirospaciari Jul 14, 2026
5a312c3
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 14, 2026
af4d380
test: probe the half-close teardown over the default-localhost path too
cirospaciari Jul 14, 2026
8a658d7
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 14, 2026
916afff
test: traced verbatim twin of the half-close teardown in the same run…
cirospaciari Jul 14, 2026
7f1b0f2
test: also probe the PING-flood teardown over the vendored test's exa…
cirospaciari Jul 14, 2026
49e01a3
test: harden the half-close trace and add a 127.0.0.1 sibling in the …
cirospaciari Jul 14, 2026
8905777
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 14, 2026
3db5527
test: half-close trace v3 - a ref'd ticker to test whether the loop t…
cirospaciari Jul 14, 2026
cb1b961
test: turn the v4 half-close trace into the timerless A/B control
cirospaciari Jul 14, 2026
173ca07
test: pin unref'd-timer firing under pure socket-wait
cirospaciari Jul 14, 2026
2a9a332
usockets(windows): give the libuv path a real bun_tick - always poll,…
cirospaciari Jul 14, 2026
b7e7338
usockets(windows): include time.h for struct timespec in the libuv bu…
cirospaciari Jul 14, 2026
671f12e
usockets(windows): us_loop_pump must also bypass uv_run's alive-guard
cirospaciari Jul 14, 2026
c1f6b65
server: keep the graceful-stop ref until the drain completes; revert …
cirospaciari Jul 14, 2026
349b56b
test: un-quarantine the Windows net-reset trio
cirospaciari Jul 14, 2026
662ee8c
Revert the Windows tick and graceful-stop keepalive changes
cirospaciari Jul 14, 2026
759e4b2
test: remove the wedge-investigation traced twins
cirospaciari Jul 14, 2026
0f4370e
node:http server: release request accounting on tunnel handoff; fix w…
cirospaciari Jul 14, 2026
f28abe6
server: keep the graceful-stop ref until the drain completes
cirospaciari Jul 14, 2026
12d4e41
test: traced verbatim twin of the PING-flood test
cirospaciari Jul 14, 2026
209e15c
test: tape the wire frames the raw server receives in the late-RST twin
cirospaciari Jul 14, 2026
6300e69
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 14, 2026
be17e34
Bound the retry of unclassified send() errnos and surface them as errors
cirospaciari Jul 14, 2026
d6a6e43
usockets(windows): deliver peer resets to write-only and paused polls
cirospaciari Jul 14, 2026
deb14ba
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 14, 2026
a4a3e07
fix the libuv abort patch's hunk headers (git-generated against the p…
cirospaciari Jul 14, 2026
4f94249
test: extend the flood tracer's watchdog past done with client-state …
cirospaciari Jul 14, 2026
b4b48f3
usockets(windows): a reset behind buffered data must not defer forever
cirospaciari Jul 14, 2026
bee36cb
node:http2: don't close a transport whose fatal write already drained
cirospaciari Jul 14, 2026
a5ba437
usockets(windows): the paused-probe must follow socket adoption and s…
cirospaciari Jul 14, 2026
776cb49
usockets(windows): sweep-escalate deferred FINs whose peer later reset
cirospaciari Jul 14, 2026
8939a7e
usockets(windows): observe a peer reset with a zero-byte send, not SO…
cirospaciari Jul 14, 2026
4e96ac2
usockets(windows): use raw winsock send for the reset probe (bsd_send…
cirospaciari Jul 14, 2026
312e071
Merge branch 'main' into claude/node-http-http2-compat
cirospaciari Jul 14, 2026
69ac4fd
test: retire the PING-flood tracer - its diagnosis is complete
cirospaciari Jul 14, 2026
223aca6
test: drop test-http2-ping-flood.js - vendored by this branch, unpass…
cirospaciari Jul 14, 2026
c51f8a4
test: give the stream-release check an immediate-drain per GC pass
cirospaciari Jul 14, 2026
a0e2dfb
node:net: server sockets must emit 'error' from native error dispatches
cirospaciari Jul 14, 2026
30dd839
node:net: revert the delegated-error guard bypass - the drain and rea…
cirospaciari Jul 14, 2026
eb693c9
test: stress twin for the worker transfer+terminate ASAN abort
cirospaciari Jul 14, 2026
947cef1
test: scrub upstream-literal markers from newly vendored node tests
robobun Jul 14, 2026
00eaa58
Revert "test: scrub upstream-literal markers from newly vendored node…
cirospaciari Jul 14, 2026
60bce3b
node:net: surface fatal-flush errors on server sockets; scope verify …
robobun Jul 14, 2026
79d4801
ci: retrigger
robobun Jul 15, 2026
eb7b498
Merge remote-tracking branch 'origin/claude/node-http-http2-compat' i…
cirospaciari Jul 15, 2026
93867b2
Merge remote-tracking branch 'origin/main' into claude/node-http-http…
robobun Aug 3, 2026
62a2fec
trim comments to <=3 lines, cite spec/node source
robobun Aug 3, 2026
2256eed
fix build: drop stale mimalloc patch and duplicate Handler.server field
robobun Aug 4, 2026
fbe7dd9
Merge remote-tracking branch 'origin/main' into claude/node-http-http…
robobun Aug 4, 2026
1057479
Merge remote-tracking branch 'origin/main' into claude/node-http-http…
robobun Aug 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/bun-uws/src/HttpParser.h
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,13 @@ struct HttpResponseData;

// Move past comma if present
if (pos < value.length() && value[pos] == ',') {
/* llhttp HPE_INVALID_TRANSFER_ENCODING: any list element after "chunked"
* (including "chunked,", "chunked,chunked") is invalid, never framed as
* chunked — https://github.com/nodejs/llhttp (src/llhttp/http.ts). */
if (te.chunked) [[unlikely]] {
te.invalid = true;
return te;
}
pos++;
}
}
Expand Down
8 changes: 8 additions & 0 deletions src/js/internal/tls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,13 @@ function secureProtocolToVersionRange(secureProtocol) {
return null;
}

// Option-ingestion rule only (node v26.3.0 internal/tls/wrap.js:1368,1686,1762):
// `!== false`, so undefined keeps verification on. Read-sites of the stored
// field use plain truthiness there (:490,:845,:1220) — do not reuse this here.
function normalizeRejectUnauthorized(value) {
return value !== false;
}

let NativeSecureContext;

/**
Expand Down Expand Up @@ -182,6 +189,7 @@ export {
VALID_TLS_ERROR_MESSAGE_TYPES,
isValidTLSArray,
isValidTLSItem,
normalizeRejectUnauthorized,
processPfxOptions,
secureProtocolToVersionRange,
throwOnInvalidTLSArray,
Expand Down
2 changes: 1 addition & 1 deletion src/js/node/net.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1013,7 +1013,7 @@ const ServerHandlers: SocketHandler<NetSocket> = {
data.destroy(error);
} else if (
data.isServer &&
data._rejectUnauthorized &&
data._rejectUnauthorized !== false &&
/peer did not return a certificate/.test(error?.message)
) {
// Ignore server's authorization errors
Expand Down
8 changes: 8 additions & 0 deletions src/js/node/tls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const {
tlsStringToProtocolVersion,
secureProtocolToVersionRange,
processPfxOptions,
normalizeRejectUnauthorized,

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.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use normalizeRejectUnauthorized as the only normalizer.

Line 576 already converts every defined non-boolean value to true. Therefore, Lines 611-614 do not normalize null, 0, or "" in this function.

Replace the earlier conversion with the shared helper. Then remove the later duplicate block.

Proposed refactor
   const rejectUnauthorized = options.rejectUnauthorized;
-  if (rejectUnauthorized !== undefined && typeof rejectUnauthorized !== "boolean") {
-    options = { ...options, rejectUnauthorized: true };
+  if (rejectUnauthorized !== undefined) {
+    options = {
+      ...options,
+      rejectUnauthorized: normalizeRejectUnauthorized(rejectUnauthorized),
+    };
   }
...
-  const rejectUnauthorized = options.rejectUnauthorized;
-  if (rejectUnauthorized !== undefined && typeof rejectUnauthorized !== "boolean") {
-    options = { ...options, rejectUnauthorized: normalizeRejectUnauthorized(rejectUnauthorized) };
-  }

As per coding guidelines, “Prefer the simplest honest shape” and use “named helpers for repeated blocks.”

Also applies to: 608-614

🤖 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` at line 13, Use normalizeRejectUnauthorized as the sole
normalization path in the surrounding TLS options handling: replace the earlier
inline conversion with this shared helper, ensuring null, 0, and empty-string
values are handled consistently, then remove the duplicate normalization block
later in the same flow.

Source: Coding guidelines

validateSecureProtocol,
} = require("internal/tls");
const {
Expand Down Expand Up @@ -604,6 +605,13 @@ function newNativeSecureContext(options, cached = false) {
options = { ...options, minVersion, maxVersion };
}
}
// Node treats any value other than an explicit `false` as "verify"; the native converter
// only accepts real booleans, so normalize the falsy-but-not-false spellings Node accepts
// (0, "", null) before they can throw or silently disable verification.
const rejectUnauthorized = options.rejectUnauthorized;
if (rejectUnauthorized !== undefined && typeof rejectUnauthorized !== "boolean") {
options = { ...options, rejectUnauthorized: normalizeRejectUnauthorized(rejectUnauthorized) };
}
const ctx = (cached ? NativeSecureContext.intern : NativeSecureContext.createPrivate)(options);
if (pfxExtraCAs) {
for (const pem of pfxExtraCAs) ctx.addCACert(pem);
Expand Down
8 changes: 6 additions & 2 deletions src/jsc/bindings/node/http/JSConnectionsList.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ JSArray* JSConnectionsList::idle(JSGlobalObject* globalObject)
size_t i = 0;
while (iter->next(globalObject, item)) {
JSHTTPParser* parser = dynamicDowncast<JSHTTPParser>(item);
if (!parser) {
// A close()d parser has no impl but stays in the list (remove() after close()
// is a no-op, matching Node); it must be skipped, not dereferenced.
if (!parser || !parser->impl()) {
continue;
}

Expand Down Expand Up @@ -143,7 +145,9 @@ JSArray* JSConnectionsList::expired(JSGlobalObject* globalObject, uint64_t heade
size_t i = 0;
while (iter->next(globalObject, item)) {
JSHTTPParser* parser = dynamicDowncast<JSHTTPParser>(item);
if (!parser) {
// A close()d parser has no impl but stays in the list (remove() after close()
// is a no-op, matching Node); it must be skipped, not dereferenced.
if (!parser || !parser->impl()) {
continue;
}

Expand Down
88 changes: 67 additions & 21 deletions src/runtime/api/bun/h2/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1011,7 +1011,8 @@ impl Connection {
let cap = (self.enforced_max_header_list_size as usize).max(65536);
if self.header_block.len().saturating_add(payload.len()) > cap {
// nghttp2's NGHTTP2_MAX_HEADERSLEN (65536) overflow returns NGHTTP2_ERR_HEADER_COMP,
// which node surfaces as a session COMPRESSION_ERROR.
// which node surfaces as a session COMPRESSION_ERROR
// (test-http2-options-max-headers-exceeds-nghttp2.js).
self.send_go_away(sink, ErrorCode::CompressionError, b"header block too large");
return true;
}
Expand Down Expand Up @@ -1066,6 +1067,11 @@ impl Connection {
let mut saw_connect = false;
let mut saw_host = false;
let mut informational = false;
// nghttp2 check_path() flags for the RFC 9113 §8.3.1 :path validation.
let mut path_regular = false;
let mut path_asterisk = false;
let mut scheme_http = false;
let mut meth_options = false;
let mut content_length: Option<u64> = None;
while off < block.len() {
match self.hpack.decode(&block[off..]) {
Expand Down Expand Up @@ -1106,11 +1112,14 @@ impl Connection {
b"protocol" => pseudo::PROTOCOL,
_ => pseudo::UNKNOWN,
};
// 8.3.1: requests never carry :status - a server seeing it inbound is
// a malformed block. (The client direction also constrains pseudo
// headers, but inbound PUSH_PROMISE blocks legitimately carry request
// pseudo-headers, so that check needs the push context first.)
let wrong_direction = self.is_server && rest == b"status";
// RFC 9113 §8.3.1/§8.3.2: :status only in response blocks, request
// pseudo-headers only in request blocks. Key on `is_request` (not
// is_server) — a client-received PUSH_PROMISE is a request block.
let wrong_direction = if is_request {
bit == pseudo::STATUS
} else {
bit != pseudo::STATUS && bit != pseudo::UNKNOWN
};
// RFC 8441 §4: :protocol is only valid when SETTINGS_ENABLE_CONNECT_PROTOCOL
// has been enabled by this endpoint. nghttp2 (and so node) checks the
// submitted local value here, not the ACKed one — so a request that arrives
Expand All @@ -1120,9 +1129,9 @@ impl Connection {
let protocol_disabled = self.is_server
&& rest == b"protocol"
&& self.local_settings.enable_connect_protocol == 0;
// nghttp2 (check_pseudo_header) treats an empty pseudo-header value as
// malformed, so `:path: ""` never counts as a present :path (§8.3.1:
// `:path` "MUST NOT be empty" for http/https).
// RFC 9113 §8.1: pseudo-headers never appear in a trailer section.
// nghttp2 (check_pseudo_header) also treats an empty pseudo-header
// value as malformed, so `:path: ""` never counts as a present :path.
if seen_regular
|| bit == pseudo::UNKNOWN
|| (seen_pseudo & bit) != 0
Expand All @@ -1137,15 +1146,43 @@ impl Connection {
informational = true;
}
seen_pseudo |= bit;
if rest == b"method" && value_b == b"CONNECT" {
saw_connect = true;
// nghttp2 http_request_on_header: per-field flags for check_path()/
// nghttp2_http_on_request_headers below; CONNECT on a pushed (even)
// stream is rejected up front ("we won't allow CONNECT for push").
match rest {
b"method" => {
if value_b == b"CONNECT" {
if push_parent != 0 {
malformed = true;
}
saw_connect = true;
}
meth_options |= value_b == b"OPTIONS";
}
b"path" => {
path_regular |= value_b.first() == Some(&b'/');
path_asterisk |= value_b == b"*";
}
b"scheme" => {
scheme_http |= value_b.eq_ignore_ascii_case(b"http")
|| value_b.eq_ignore_ascii_case(b"https");
}
_ => {}
}
} else {
seen_regular = true;
match name_b {
b"connection" | b"keep-alive" | b"proxy-connection"
| b"transfer-encoding" | b"upgrade" => malformed = true,
b"host" if is_request => saw_host = true,
// nghttp2 http_request_on_header: in request blocks Host is checked
// like :authority (empty/repeated => malformed); in a response it
// is an ordinary field and node delivers it.
b"host" if self.is_server || is_request => {
if value_b.is_empty() || saw_host {
malformed = true;
}
saw_host = true;
}
Comment on lines +1180 to +1185

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Restrict the Host validation to request blocks.

The guard is self.is_server || is_request. On a server, is_request is self.is_server && is_new, so it is false for a trailer section. The self.is_server disjunct therefore also applies the :authority-style Host rules to inbound request trailers.

nghttp2 routes a trailer block through http_trailer_on_header, which has no host case. A trailer carrying host: "" (or a repeated host) is delivered by node, but this code marks the block malformed and answers RST_STREAM(PROTOCOL_ERROR).

The comment above the arm states the intent as "in request blocks Host is checked like :authority". is_request already covers both inbound HEADERS request blocks and client-received PUSH_PROMISE blocks, so the extra disjunct is not needed.

🐛 Proposed fix to scope the check to request blocks
-                                b"host" if self.is_server || is_request => {
+                                b"host" if is_request => {
                                     if value_b.is_empty() || saw_host {
                                         malformed = true;
                                     }
                                     saw_host = true;
                                 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
b"host" if self.is_server || is_request => {
if value_b.is_empty() || saw_host {
malformed = true;
}
saw_host = true;
}
b"host" if is_request => {
if value_b.is_empty() || saw_host {
malformed = true;
}
saw_host = true;
}
🤖 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/api/bun/h2/connection.rs` around lines 1180 - 1185, Restrict the
`b"host"` validation arm to `is_request` only by removing the `self.is_server`
disjunct. Preserve the existing empty-value and duplicate-Host checks for
request blocks, while allowing trailer sections to bypass this validation.

b"te" => {
// RFC 9110 10.1.4: field values are case-insensitive.
if !value_b.eq_ignore_ascii_case(b"trailers") {
Expand Down Expand Up @@ -1199,11 +1236,9 @@ impl Connection {
sink.on_stream_reset(target, ErrorCode::StreamClosed.as_u32());
return false;
}
// RFC 9113 §8.3.1 (nghttp2_http_on_request_headers): a request block needs exactly one
// non-empty :method, :scheme and :path plus an :authority or Host; plain CONNECT omits
// :scheme/:path and carries :authority; extended CONNECT (:protocol, RFC 8441) requires
// :method CONNECT and :authority. Without this a block with an empty or missing :path
// reaches JS as a request with an empty url (no compliant peer can produce that shape).
// RFC 9113 §8.3.1 / nghttp2_http_on_request_headers: request block = :method+:scheme
// +:path + (:authority|Host); plain CONNECT omits :scheme/:path with :authority;
// extended CONNECT (RFC 8441) needs :method CONNECT. Applies to HEADERS & PUSH_PROMISE.
if is_request && !rejected && !malformed {
use pseudo::{AUTHORITY, METHOD, PATH, PROTOCOL, SCHEME};
let extended_connect = (seen_pseudo & PROTOCOL) != 0;
Expand All @@ -1213,8 +1248,19 @@ impl Connection {
(seen_pseudo & (METHOD | SCHEME | PATH)) != (METHOD | SCHEME | PATH)
|| ((seen_pseudo & AUTHORITY) == 0 && !saw_host)
Comment thread
alii marked this conversation as resolved.
|| (extended_connect && (!saw_connect || (seen_pseudo & AUTHORITY) == 0))
// nghttp2 check_path(): under http/https, :path must start with '/'
// (or be '*' for OPTIONS).
|| (scheme_http && !(path_regular || (meth_options && path_asterisk)))
};
}
} else if !is_trailer && !rejected && !malformed && !informational {
// RFC 9113 §8.3.2 (nghttp2_http_on_response_headers): a final response block must
// carry exactly :status and no request pseudo-header. wrong_direction above already
// rejected a request pseudo per-field; this catches a block with :status omitted.
malformed = (seen_pseudo & pseudo::STATUS) == 0;
}
// RFC 9113 §8.1.1: an inbound request's content-length must be coherent — the declared
// value is attached to the stream and, at END_STREAM, must equal the DATA received
// (plain CONNECT is exempt).
if push_parent == 0 && self.is_server && !malformed && !rejected {
if let Some(s) = self.streams.get_mut(&target) {
if !saw_connect && s.content_length.is_none() {
Expand All @@ -1229,9 +1275,9 @@ impl Connection {
}
}
if malformed && !rejected {
// node (Http2Session::OnInvalidFrame): every locally-rejected invalid frame counts
// against maxSessionInvalidFrames; exceeding it tears the session down with
// ERR_HTTP2_TOO_MANY_INVALID_FRAMES (same post-increment comparison as node).
// nghttp2 session_handle_invalid_stream2 / RFC 9113 §8.4.1: malformed HEADERS or
// PUSH_PROMISE → RST_STREAM(PROTOCOL_ERROR) on the target id + invalid-frame count.
// node Http2Session::OnInvalidFrame tears down on maxSessionInvalidFrames overflow.
let count = self.invalid_frame_count;
self.invalid_frame_count = count.saturating_add(1);
if count > self.max_invalid_frames {
Expand Down
79 changes: 55 additions & 24 deletions src/runtime/api/bun/h2_frame_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5139,6 +5139,8 @@ impl H2FrameParser {
if global.has_exception() {
return Some(stream);
}
// The `*mut Stream` above stays live across this call into JS.
let _dispatch = self.enter_dispatch();
match callback.call(
&global,
ctx_value,
Expand Down Expand Up @@ -5498,6 +5500,34 @@ impl H2FrameParser {
});
}

/// Free streams whose legacy lifecycle finished (queued by `free_resources`). Only runs
/// at a quiescent point — no JS dispatch on the stack and no in-progress receive()
/// borrowing the engine cell; otherwise ids stay queued for the next such point.
fn drain_pending_engine_stream_closes(&self) {
if self.dispatch_depth.get() != 0 || self.pending_engine_stream_closes.get().is_empty() {
return;
}
let Ok(mut engine_guard) = self.engine.try_borrow_mut() else {
return;
};
let Some(engine) = engine_guard.as_mut() else {
return;
};
self.pending_engine_stream_closes.with_mut(|v| {
for id in v.drain(..) {
engine.close_stream(id);
if let Some(stream) = self.streams.with_mut(|m| m.remove(&id)) {
// SAFETY: sole owner just removed from the map; free_resources already ran;
// dispatch-depth gate above proves no native frame still borrows it; stream
// ids never repeat in a session → frees exactly once.
unsafe {
drop(bun_core::heap::take(stream));
}
}
}
});
}

/// Feed inbound bytes through the rewrite engine, buffering the unconsumed tail (design B).
fn rewrite_read(&self, bytes: &[u8]) {
bun_output::scoped_log!(H2FrameParser, "rewriteRead {}", bytes.len());
Expand Down Expand Up @@ -5560,28 +5590,11 @@ impl H2FrameParser {
engine.pending_local_settings_acks.push_back(w);
}
});
// Streams whose legacy lifecycle finished since the last batch: evict the engine
// entry and free the legacy slot. free_resources already ran for these (it is the
// only producer of this queue); duplicate ids are fine — remove() yields None.
if self.dispatch_depth.get() == 0 {
self.pending_engine_stream_closes.with_mut(|v| {
for id in v.drain(..) {
engine.close_stream(id);
if let Some(stream) = self.streams.with_mut(|m| m.remove(&id)) {
// SAFETY: stream is the heap::alloc'd *mut Stream owned by the
// map entry just removed; free_resources ran when it was queued,
// dispatch_depth == 0 means no caller below us on the stack holds
// a `&mut Stream` across anything that can run user JS (every
// such site arms enter_dispatch), ids never repeat within a
// session, so this frees exactly once.
unsafe {
drop(bun_core::heap::take(stream));
}
}
}
});
}
}
// Streams whose legacy lifecycle finished since the last batch: evict the engine entry
// and free the legacy slot at this quiescent point (the helper enforces the safety
// rules; deferred ids are also reclaimed at the next host-call boundary).
self.drain_pending_engine_stream_closes();
if self.rewrite_tail.get().is_empty() {
let feed = {
let mut guard = self.engine.borrow_mut();
Expand Down Expand Up @@ -7045,6 +7058,9 @@ impl H2FrameParser {
callframe: &CallFrame,
) -> JsResult<JSValue> {
bun_output::scoped_log!(H2FrameParser, "rstStream");
// Quiescent host-call boundary: reclaim deferred stream closes before this frame
// materializes any `*mut Stream`.
this.drain_pending_engine_stream_closes();
let [stream_arg, error_arg] = callframe.arguments_as_array::<2>();
if callframe.arguments_count() < 2 {
return Err(global_object.throw(format_args!("Expected stream and code arguments")));
Expand Down Expand Up @@ -7912,6 +7928,10 @@ impl H2FrameParser {
defer_callback_arg,
] = args.ptr;

// Quiescent host-call boundary: reclaim deferred stream closes before this frame
// materializes any `*mut Stream`.
this.drain_pending_engine_stream_closes();

if !stream_arg.is_number() {
return Err(global_object.throw(format_args!("Expected stream to be a number")));
}
Expand Down Expand Up @@ -7956,9 +7976,18 @@ impl H2FrameParser {
}
};

let buffer = match StringOrBuffer::from_js_with_encoding(global_object, data_arg, encoding)?
{
Some(b) => b,
// send_data can re-enter JS mid-payload (batch flushes, prior writes' callbacks): pin
// + protect ArrayBuffer payloads so they can't be detached under the borrowed slice.
// Strings are immutable (zero-copy path); ThreadSafe's Drop releases the pin/protect.
let pin_payload = data_arg.is_cell() && data_arg.js_type().is_array_buffer_like();
let buffer = match StringOrBuffer::from_js_with_encoding_maybe_async(
global_object,
data_arg,
encoding,
pin_payload,
true,
)? {
Some(b) => bun_jsc::ThreadSafe::adopt(b),
Comment on lines +7979 to +7990

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.

🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Resolve StringOrBuffer::from_js_with_encoding_maybe_async and ThreadSafe::adopt signatures and Drop.
set -euo pipefail

echo "== from_js_with_encoding_maybe_async =="
rg -n -C 20 'fn from_js_with_encoding_maybe_async' src

echo "== ThreadSafe definition, adopt, Drop =="
rg -n -C 10 'pub struct ThreadSafe|impl .*ThreadSafe|fn adopt' src/jsc src/bun_jsc 2>/dev/null || rg -n -C 10 'ThreadSafe' src

echo "== other call sites for comparison =="
rg -n -C 3 'from_js_with_encoding_maybe_async\(' src

Repository: oven-sh/bun

Length of output: 50368


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Definitions =="
rg -n -C 18 --glob '*.rs' 'from_js_with_encoding_maybe_async|pub struct ThreadSafe|impl<.*ThreadSafe|impl ThreadSafe|fn adopt|impl.*Unprotect|fn unprotect' src/runtime src | head -n 1200

echo "== Target call sites =="
rg -n -C 8 --glob '*.rs' 'from_js_with_encoding_maybe_async\(' src/runtime/api/bun/h2_frame_parser.rs src/runtime/node src/runtime/api/BunObject.rs

echo "== ThreadSafe::adopt call sites =="
rg -n -C 8 --glob '*.rs' 'ThreadSafe::adopt\(' src/runtime src | head -n 800

Repository: oven-sh/bun

Length of output: 50368


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Target implementation and call =="
sed -n '7750,8010p' src/runtime/api/bun/h2_frame_parser.rs | cat -n
sed -n '285,325p' src/runtime/node/types.rs | cat -n
sed -n '495,580p' src/runtime/node/types.rs | cat -n

echo "== ThreadSafe definition and impl =="
rg -n --glob '*.rs' --glob '*.zig' --glob '*.ziggy' \
  'pub struct ThreadSafe|struct ThreadSafe|impl[^\\n]*ThreadSafe|pub fn adopt|fn adopt|trait Unprotect' . \
  | rg 'ThreadSafe|Unprotect' | head -n 300

echo "== Relevant ThreadSafe call sites =="
rg -n -C 12 --glob '*.rs' 'bun_jsc::ThreadSafe::adopt\(' src/runtime/api/bun/h2_frame_parser.rs src/runtime/api/JSTranspiler.rs src/runtime/api/BunObject.rs src/runtime/crypto/PBKDF2.rs src/runtime

Repository: oven-sh/bun

Length of output: 35862


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,110p' src/jsc/node_path.rs | cat -n
rg -n -C 8 --glob '*.rs' 'ThreadSafe<' src/jsc src/runtime | head -n 500
rg -n -C 12 --glob '*.rs' 'impl.*Unprotect for StringOrBuffer|from_js_maybe_async_into' src/runtime/node/types.rs

Repository: oven-sh/bun

Length of output: 35568


Label both conversion flags

The fourth argument is is_async; the fifth is allow_string_object. Keep these labels visible at the call site. ThreadSafe::adopt safely takes ownership of the existing protection and releases it once on drop.

🤖 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/api/bun/h2_frame_parser.rs` around lines 7979 - 7990, Update the
StringOrBuffer::from_js_with_encoding_maybe_async call in the send_data
conversion path to use explicit argument labels/comments for the fourth is_async
flag and fifth allow_string_object flag, preserving their current true values.
Leave ThreadSafe::adopt unchanged because it already takes ownership of the
existing protection and releases it exactly once.

None => {
return Err(global_object.throw_invalid_argument_type_value(
b"write",
Expand Down Expand Up @@ -8347,6 +8376,8 @@ impl H2FrameParser {
};
let mut _count: u32 = 0;
let mut it = StreamResumableIterator::init(this);
// The iterator's `*mut Stream`s stay live across the callbacks below.
let _dispatch = this.enter_dispatch();
while let Some(stream) = it.next() {
// SAFETY: stream is *mut Stream from self.streams; valid while the map entry exists
let Some(value) = (unsafe { (*stream).js_context.get() }) else {
Expand Down
Loading