Skip to content

node: internalBinding http2/StreamBase/js_stream_socket handle shims v26 compat (+16 tests) - #35522

Draft
cirospaciari wants to merge 14 commits into
claude/node-v26-fix-tlsfrom
claude/node-binding-shims-v26
Draft

node: internalBinding http2/StreamBase/js_stream_socket handle shims v26 compat (+16 tests)#35522
cirospaciari wants to merge 14 commits into
claude/node-v26-fix-tlsfrom
claude/node-binding-shims-v26

Conversation

@cirospaciari

Copy link
Copy Markdown
Member

Adds the internalBinding/native-handle test families: the http2 *-errors suite, the StreamBase JS-handle protocol suite (stream_wrap / js_stream / tcp_wrap), and the internal/js_stream_socket suite. 16 new upstream tests vendored byte-identical from Node v26.3.0, all passing; 3 more already-vendored tests (test-stream-wrap, test-stream-wrap-drain, test-stream-wrap-encoding) now pass but stay quarantined in test/expectations.txt lines 53-55 (removing those needs a main-side expectations PR).

What was missing

These tests monkey-patch methods on node's native handle prototypes (internalBinding('http2').Http2Stream.prototype.respond = () => code, internalBinding('js_stream').JSStream, internalBinding('tcp_wrap').TCP) to inject failures or drive I/O by hand. Bun's runtime drives its engines directly and has no handle layer, so internalBinding reported them unimplemented.

Shims (test-only layer)

All in src/js/internal/test/binding.ts (expose-internals-gated) and test/js/node/test/common/nodeinternals/:

  • http2: stand-in Http2Session/Http2Stream handle classes; the first internalBinding('http2') call wraps the real node:http2 entry points node routes through the handle (request / respond / respondWithFile / additionalHeaders / pushStream) to consult those prototypes at call time and replicate node's dispatch from lib/internal/http2/core.js (ERR_HTTP2_OUT_OF_STREAMS / ERR_HTTP2_STREAM_SELF_DEPENDENCY / NghttpError; session-vs-stream error targets; pushStream's callback delivery). With nothing stubbed every wrapper falls through to the real implementation, so the shim is inert outside these tests. internal/http2/util.js is vendored byte-identical; the harness recovers the live NghttpError/assert helpers so instanceof/constructor checks match, and bridges the public http2.sensitiveHeaders symbol onto the vendored module's private one.
  • stream_wrap / js_stream: WriteWrap/ShutdownWrap and the shared streamBaseState array (src/stream_base.h layout), plus a JS JSStream implementing node's callback-driven handle (readBuffer / emitEOF / finishWrite / finishShutdown; extends TextEncoder so structured clone still rejects instances as host objects, which the worker transfer tests assert).
  • tcp_wrap: TCP with bind/getsockname/listen/connect/readStart/writeBuffer/shutdown over Bun.listen/Bun.connect (both bind synchronously, like uv_tcp_bind). The raw-fd surface the vendored dgram tests use is preserved.
  • internal/js_stream_socket: node's class surface verbatim (doShutdown/finishShutdown/doWrite/finishWrite/doClose and the pending-write/shutdown bookkeeping), with the Duplex plumbing node gets from net.Socket({ handle }) written out explicitly, since Bun's net.Socket has no JS-handle StreamBase layer.
  • internal/webstreams/adapters: the two StreamBase-backed adapters vendored verbatim, merged under Bun's real adapters module.

Runtime fixes found by the tests

  • http2 (src/js/node/http2.ts): kNghttp2ErrorMessages now carries the full nghttp2_strerror() table (was an 11-entry subset, so most NghttpError messages read "Unknown error code"); http2.constants gains the 13 constants node registers with NODE_DEFINE_HIDDEN_CONSTANT (NGHTTP2_HCAT_*, NGHTTP2_NV_FLAG_*, NGHTTP2_ERR_*, STREAM_OPTION_*), defined non-enumerably like node so Object.keys(constants) is unchanged.
  • net (src/js/node/net.ts): socket.destroy() followed by the native close synthesized a graceful 'end' event node never emits (fixing line: the self.destroyed guard in finishSocketEnd). Covered by test-wrap-js-stream-destroy, which fails without it.

Base repairs (merge damage on claude/node-v26-fix-tls)

The permission-wave2 merge (3d683b2) dropped whole generations of the cli-parity work while keeping its tests. On the unfixed base, every bun -e/-p/--eval invocation prints the intro help and exits 0. Restored, content taken verbatim from the surviving commits (3c39e30 / fd183a8) with the permission-model pieces of the old world excluded so wave2's --permission handling is untouched:

  • ea236c1 cherry-picked (Ciro's existing repair: reject_bad_negations, fs path-arg messages; base does not compile without it).
  • -e/-p value binding + eval.provided dispatch (this is what un-breaks eval).
  • node-style missing-value / invalid-negation errors, NODE_OPTIONS validation.
  • --check syntax mode, --input-type, --inspect-port/--debug-port declarations.

run-eval.test.ts goes 50 pass/41 fail → 88 pass/3 fail on this branch; the vendored test-cli-{bad-options,node-options-disallowed,syntax-*} suite (already in the base, failing there) passes except test-cli-bad-options. The remaining failures are permission-model contract (--allow-fs-read without --permission must exit 1; three stale run-eval cases assert the pre-wave2 "--permission is not supported" rejection) — that belongs to the permission-model owner, not changed here.

Verified

  • All 16 vendored tests pass CI-style (BUN_GARBAGE_COLLECTOR_LEVEL=1, bunfig.node-test.toml, one process per file) and are byte-identical to upstream; preflight OK, including the silent-pass check.
  • Blast radius re-run green: the 3 quarantined stream-wrap tests, dgram raw-fd tests, worker transfer-native (JSStream clone rejection), net-listen-handle-in-cluster-2, the internal/http2/util consumers (misc-util, util-asserts, util-assert-valid-pseudoheader, util-nghttp2error), an http2 vendored sample, bun test test/js/node/net/ (219 pass/0 fail), and all 145 vendored test-net-* files (2 failures, both keepalive-delay-unit issues reproduced on the unfixed base).
  • http2: implement internalBinding('http2') for the vendored node test shim (+2 upstream tests) #34499's test-http2-binding and test-http2-respond-errors (which own the binding.ts http2 case on their branch) still pass under this extended shim.

Out of scope, flagged

  • test-http-agent-domain-reused-gc needs ReusedHandle AsyncWrap resources and _handle.asyncReset in the http agent; not vendored.
  • The permission-contract failures above.

…or messages

The merge of claude/node-v26-permission-wave2 dropped the
reject_bad_negations field from the ParseOptions initializer in
Arguments.rs (added by the cli negation-errors commit), breaking the
build, and resolved three node_fs.rs call sites back to the pre-parity
generic 'path must be a string' errors, orphaning
PathOrFdExt::from_js_required. Restores the field and the
from_js_required calls; deletes BUFFER_EXPECTED_TYPES, superseded by
throw_invalid_argument_type_list at its only former call site.
…tants

- kNghttp2ErrorMessages now carries the full nghttp2_strerror() table instead
  of an 11-entry subset, so NghttpError messages match node for every code.
- http2.constants gains the 13 constants node's binding registers with
  NODE_DEFINE_HIDDEN_CONSTANT (NGHTTP2_HCAT_*, NGHTTP2_NV_FLAG_*,
  NGHTTP2_ERR_*, STREAM_OPTION_*), defined non-enumerably like node so
  Object.keys(constants) is unchanged.
- The test-only internals surface (Symbol.for'd, consumed by the vendored
  suite's --expose-internals shim) additionally exposes nghttp2ErrorString and
  createPendingStreamCancelError.
socket.destroy() followed by the native close event ran finishSocketEnd,
which push(null)'d a graceful 'end' the way a peer FIN would. Node never
emits 'end' after a local destroy (the readable is already destroyed).
Guard the EOF synthesis on !self.destroyed; peer-FIN delivery on live
sockets is unchanged.

Covered by the vendored test-wrap-js-stream-destroy (its server asserts
'end' is not emitted after socket.destroy()), which failed on exactly this.
…util (+7 tests)

Node's http2 *-errors tests stub methods on internalBinding('http2')'s
Http2Session/Http2Stream prototypes to inject nghttp2 error codes into the
JS layer. Bun drives its engine directly with no handle layer, so the
test-only binding now exposes stand-in handle classes and, on first use,
wraps the real node:http2 entry points that node routes through the handle
(request/respond/respondWithFile/additionalHeaders/pushStream) to consult
those prototypes at call time and replicate node's error dispatch from
lib/internal/http2/core.js (ERR_HTTP2_OUT_OF_STREAMS /
ERR_HTTP2_STREAM_SELF_DEPENDENCY / NghttpError, session-vs-stream targets).
With nothing stubbed every wrapper falls through to the real implementation,
so the shim is inert outside these tests.

internal/http2/util.js is vendored byte-identical from node v26.3.0; the
require interceptor recovers the live NghttpError/assert helpers and kSocket
from node:http2's internals so instanceof checks keep matching, and bridges
the public http2.sensitiveHeaders symbol onto the vendored module's private
one for buildNgHeaderString. The binding also carries the shared state
arrays (settingsBuffer/optionsBuffer/sessionState/streamState) and
refreshDefaultSettings the vendored module destructures.

Vendored (all pass, byte-identical to upstream):
  test-http2-client-onconnect-errors, test-http2-info-headers-errors,
  test-http2-respond-nghttperrors, test-http2-respond-with-fd-errors,
  test-http2-server-push-stream-errors, test-http2-util-headers-list,
  test-http2-util-update-options-buffer
…_wrap (+9 tests)

The test-only binding now implements the StreamBase JS-handle protocol the
stream_wrap/js_stream/tcp_wrap tests exercise:

- internalBinding('stream_wrap'): WriteWrap/ShutdownWrap request classes and
  the shared streamBaseState array (kReadBytesOrError/kArrayBufferOffset/
  kBytesWritten/kLastWriteWasAsync, src/stream_base.h layout).
- internalBinding('js_stream').JSStream: node's JS-callback-driven handle
  (readBuffer/emitEOF/finishWrite/finishShutdown + onread/onwrite/onshutdown
  hooks). Extends TextEncoder so instances stay host objects — the vendored
  worker tests require postMessage(new JSStream()) to throw DataCloneError.
- internalBinding('tcp_wrap').TCP: bind/bind6/getsockname/listen/connect/
  readStart/writeBuffer/shutdown over Bun.listen/Bun.connect (which bind
  synchronously, like uv_tcp_bind). The raw-fd path the vendored dgram tests
  use (handle.fd + zero-argument listen()) is preserved.

internal/js_stream_socket is a nodeinternals shim keeping node's class
surface (doShutdown/finishShutdown/doWrite/finishWrite/doClose, pending-
write/shutdown bookkeeping verbatim) with the Duplex plumbing node gets from
net.Socket({ handle }) written out explicitly, since Bun's net.Socket has no
JS-handle StreamBase layer. internal/webstreams/adapters vendors the two
StreamBase-backed adapters verbatim; the interceptor merges them under Bun's
real adapters module.

Vendored (all pass, byte-identical to upstream):
  test-wrap-js-stream-{destroy,duplex,exceptions,read-stop},
  test-js-stream-call-properties, test-tcp-wrap, test-tcp-wrap-connect,
  test-tcp-wrap-listen, test-whatwg-webstreams-adapters-streambase

Also fixed by this + the net 'end' fix: the already-vendored
test-stream-wrap, test-stream-wrap-drain, test-stream-wrap-encoding
(quarantined in test/expectations.txt lines 53-55; removing those lines
needs a main-side expectations PR).
The claude/node-v26-permission-wave2 merge resolved Arguments.rs back to a
pre-parity state: the -e/-p params lost their node-style value-binding
markers and the --print/--eval handling no longer set eval.provided, which
is what Command dispatch selects eval mode on — every bun -e/-p/--eval
invocation printed the intro help and exited 0. Restores the param markers
and the final --print/--eval block from the cli value-binding commit
(3c39e30). Same damage class ea236c1 repaired.
…S validation lost in branch merge

Second half of the same merge damage: the parse error path lost Node's
'<execPath>: <flag> requires an argument' / invalid-negation reporting
(exit 9), and the node-standing commands lost NODE_OPTIONS validation with
Node's disallowed-option contract. Content restored verbatim from the cli
value-binding commit (3c39e30); the clap-side machinery survived the
merge, so this is Arguments.rs only.
…sing lost in branch merge

Third tranche of the same merge damage: the runtime param tables lost the
--check declaration (with -c handed to it on the node-standing commands via
BASE_PARAMS_NO_CONFIG_SHORT_), --input-type (dispatch read the option but no
table declared it), and --inspect-port/--debug-port. Restored from the cli
--check commit (fd183a8), except its permission-flag rejection block and
duplicate --permission/--allow-fs-* params: the merged permission model
(claude/node-v26-permission-wave2) now parses and enforces those, so the old
loud rejection must not resurface.
@robobun

robobun commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator
Updated 1:05 AM PT - Aug 4th, 2026

@robobun, your commit fcac658 has 84 failures in Build #88612 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 35522

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

bun-35522 --bun

robobun added 6 commits August 3, 2026 20:40
…aude/node-binding-shims-v26

# Conflicts:
#	src/runtime/cli/Arguments.rs
…aude/node-binding-shims-v26

# Conflicts:
#	src/js/internal/test/binding.ts
…aude/node-binding-shims-v26

# Conflicts:
#	src/jsc/bindings/BunHeapProfiler.h
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.

2 participants