Skip to content

node:http2: add the hidden NGHTTP2_* and STREAM_OPTION_* constants - #41546

Open
robobun wants to merge 1 commit into
mainfrom
robobun/ab6b58d0/http2-hidden-constants
Open

robobun wants to merge 1 commit into
mainfrom
robobun/ab6b58d0/http2-hidden-constants

Conversation

@robobun

@robobun robobun commented Sep 6, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • require("http2").constants lacks 13 entries node exports: NGHTTP2_HCAT_REQUEST/RESPONSE/PUSH_RESPONSE/HEADERS, NGHTTP2_NV_FLAG_NONE/NO_INDEX, NGHTTP2_ERR_DEFERRED/STREAM_ID_NOT_AVAILABLE/INVALID_ARGUMENT/STREAM_CLOSED/NOMEM, STREAM_OPTION_EMPTY_PAYLOAD/GET_TRAILERS. A comparison such as code === constants.NGHTTP2_ERR_STREAM_CLOSED reads undefined and never matches.
  • Node defines them in node_http2.cc with NODE_DEFINE_HIDDEN_CONSTANT. They are read-only and not enumerable, so a key-by-key diff of Object.keys does not show them. Bun's constants literal in src/js/node/http2.ts:1452 never had them.

Fix

  • Define the 13 entries on the constants object with Object.defineProperties after the literal. Each descriptor is { value } only, so the property is not writable, not enumerable, and not configurable, the same as node.
  • Object.keys(constants) stays at 240 entries, as in node v26.3.0.
  • Verified: test/js/node/http2/node-http2.test.js (new test, fails on bun 1.4.3, passes with this change).

Background

  • http2.constants mirrors nghttp2's enums and node's own stream option flags. Node fills it from C++ and marks a few entries hidden so they do not show up in enumeration but still resolve by name.
  • Bun's node:http2 is a pure JS module with a hand-written constants literal. The literal can only express enumerable keys, so the hidden set needs defineProperties.

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

fails on main (without fix)
ASAN without fix: 1 failed, 6 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/http2/node-http2.test.js"
bun test v1.4.3 (f42e98025)

test/js/node/http2/node-http2.test.js:
(pass) node none > Client Basics > should be able to send a GET request [1022.52ms]
(pass) node none > Client Basics > should be able to send a POST request [724.47ms]
(pass) node none > Client Basics > constants [29.80ms]
(pass) node none > Client Basics > getDefaultSettings [10.68ms]
(pass) node none > Client Basics > getPackedSettings/getUnpackedSettings [27.93ms]
(pass) node none > Client Basics > getUnpackedSettings should throw if buffer is too small [5.86ms]
(pass) node none > Client Basics > getUnpackedSettings should throw if buffer is not a multiple of 6 bytes [3.87ms]
(pass) node none > Client Basics > getUnpackedSettings should throw if buffer is not a buffer [5.65ms]
(pass) node none > Client Basics > should be able to send data using end [739.43ms]
(pass) node none > Client Basics > should be able to mutiplex GET requests [715.86ms]
(pass) node none > Client Basics > http2 should receive remoteSettings when receiving
... (truncated)

release without fix: 1 failed, 6 skipped
bun test v1.4.3-canary.1 (f42e98025)

test/js/node/http2/node-http2.test.js:
(pass) node none > Client Basics > constants [1.00ms]
(pass) node none > Client Basics > getDefaultSettings [0.16ms]
(pass) node none > Client Basics > getPackedSettings/getUnpackedSettings [0.30ms]
(pass) node none > Client Basics > getUnpackedSettings should throw if buffer is too small [0.14ms]
(pass) node none > Client Basics > getUnpackedSettings should throw if buffer is not a multiple of 6 bytes [0.04ms]
(pass) node none > Client Basics > getUnpackedSettings should throw if buffer is not a buffer [0.09ms]
(pass) node none > Client Basics > is possible to abort request [2.10ms]
(pass) node none > Client Basics > aborted event should work with abortController [0.97ms]
(pass) node none > Client Basics > aborted event should work with aborted signal [0.82ms]
(pass) node none > Client Basics > signal validation matches node: non-signal objects throw, duck-typed { aborted } is accepted [1.28ms]
(pass) node none > Client Basics > should fail to connect over HTTP/1.1 [35.67ms]
(skip) node none > Client Basics > should not leak memory
(pass) node none > Client Basics > headers cannot be bigge
... (truncated)
passes on PR (with fix)
ASAN with fix: 6 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/http2/node-http2.test.js"
bun test v1.4.3 (f42e98025)

test/js/node/http2/node-http2.test.js:
(pass) node none > Client Basics > should be able to send a GET request [1792.59ms]
(pass) node none > Client Basics > should be able to send a POST request [917.10ms]
(pass) node none > Client Basics > constants [32.59ms]
(pass) node none > Client Basics > getDefaultSettings [12.13ms]
(pass) node none > Client Basics > getPackedSettings/getUnpackedSettings [32.57ms]
(pass) node none > Client Basics > getUnpackedSettings should throw if buffer is too small [9.79ms]
(pass) node none > Client Basics > getUnpackedSettings should throw if buffer is not a multiple of 6 bytes [5.77ms]
(pass) node none > Client Basics > getUnpackedSettings should throw if buffer is not a buffer [9.08ms]
(pass) node none > Client Basics > should be able to send data using end [970.25ms]
(pass) node none > Client Basics > should be able to mutiplex GET requests [958.27ms]
(pass) node none > Client Basics > http2 should receive remoteSettings when receiving
... (truncated)

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

24 deps, 131 codegen, 2175 objects in 4637ms

ninja: Entering directory `/workspace/bun/build/release'
[1/2863] mkdir obj
[2/2863] mkdir pch
[3/2863] mkdir codegen
[4/2863] gen ErrorCode+*.h
[5/2863] fetch mimalloc
[mimalloc] up to date
[6/2863] fetch WebKit
[WebKit] up to date
[7/2863] gen bindgenv2
[8/2863] fetch icu
[icu] up to date
[9/2863] fetch tinycc
[tinycc] up to date
[10/2863] gen .bind.ts → GeneratedBindings.cpp
[11/2863] gen bake.{client,server,error}.js
-> bake.client.js, bake.server.js, bake.error.js
[12/2863] gen ProcessBindingConstants.lut.h
Generating /workspace/bun/build/release/codegen/ProcessBindingConstants.lut.h from /workspace/bun/src/jsc/bindings/ProcessBindingConstants.cpp
[13/2863] gen JSBuffer.lut.h
Generating /workspace/bun/build/release/codegen/JSBuffer.lut.h from /workspace/bun/src/jsc/bindings/JSBuffer.cpp
[14/2863] fetch zlib
[zlib] up to date
[15/2863] fetch libjpeg-turbo
[libjpeg-turbo] up to date
[16/2863] gen
... (truncated)
diff hotspot
src/js/node/http2.ts                  | 16 ++++++++++++++++
 test/js/node/http2/node-http2.test.js | 34 ++++++++++++++++++++++++++++++++++
 2 files changed, 50 insertions(+)

gate history · 2 passed · 0 rejected · iteration 1

evidence per changed file
file                                   reads  edits  tests
src/js/node/http2.ts                       1      2      5
test/js/node/http2/node-http2.test.js      0      0      4

@coderabbitai

coderabbitai Bot commented Sep 6, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

On-demand reviews are free for the next 14 days. After that, they cost $0.25 per reviewed file.

Or wait 9 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 370e4179-ac4a-433b-9beb-fcdb0af1cd30

📥 Commits

Reviewing files that changed from the base of the PR and between d316760 and 4f7a22f.

📒 Files selected for processing (2)
  • src/js/node/http2.ts
  • test/js/node/http2/node-http2.test.js

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

@robobun

robobun commented Sep 6, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 8:04 AM PT - Sep 6th, 2026

❌ @robobun, your commit 4f7a22f has 2 failures in Build #111227 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 41546

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

bun-41546 --bun

@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 — straightforward Node compat: 13 hidden constants added to http2.constants with the correct descriptors.

What was reviewed:

  • Values cross-checked against nghttp2's nghttp2.h enums (HCAT 0-3, NV_FLAG 0/1, ERR -501/-508/-509/-510/-901) and Node's node_http2.h StreamOptions (1/2) — all match.
  • { value }-only descriptors give writable/enumerable/configurable all false, matching NODE_DEFINE_HIDDEN_CONSTANT; Object.keys(constants) is unchanged.
  • Object.defineProperties runs at module-eval time on a fresh literal, so no primordial-tampering exposure.
  • Test appended to the existing node-http2.test.js, asserts values + full descriptors + non-enumeration, and fails on current Bun where these keys are undefined.
Extended reasoning...

Overview

This PR adds 13 constants to require("node:http2").constants that Node.js defines via NODE_DEFINE_HIDDEN_CONSTANT in src/node_http2.cc: four NGHTTP2_HCAT_* header-category enums, two NGHTTP2_NV_FLAG_* flags, five NGHTTP2_ERR_* codes, and two STREAM_OPTION_* flags. They are attached with Object.defineProperties using { value }-only descriptors so the properties are non-writable, non-enumerable, and non-configurable — exactly what Node's macro produces. A test in the existing test/js/node/http2/node-http2.test.js asserts each value, the full property descriptor, and that hidden entries stay out of Object.keys(constants).

Security risks

None. This is a pure additive constants table on a built-in module, evaluated once at module load before any user code runs. No input parsing, no I/O, no auth or crypto surface. The Object.defineProperties call operates on a locally-constructed object literal at load time, so prototype-pollution / global-tampering concerns from src/js/CLAUDE.md do not apply here.

Level of scrutiny

Low. The change is mechanical Node-compat data entry. The only thing to get wrong is a numeric value or a descriptor attribute, and both were checked: the NGHTTP2_* values line up with nghttp2's public header (NGHTTP2_ERR_INVALID_ARGUMENT=-501, DEFERRED=-508, STREAM_ID_NOT_AVAILABLE=-509, STREAM_CLOSED=-510, NOMEM=-901; HCAT_* 0-3; NV_FLAG_NONE=0, NO_INDEX=1), and STREAM_OPTION_EMPTY_PAYLOAD=1 / GET_TRAILERS=2 match Node's enum StreamOptions in node_http2.h. A bare { value } descriptor defaults writable/enumerable/configurable to false per ECMA-262 §6.2.6.1, matching NODE_DEFINE_HIDDEN_CONSTANT's ReadOnly | DontDelete | DontEnum.

Other factors

The test follows repo conventions: appended to the existing module test file (not a new file), uses a composite .toEqual on the value map rather than 13 .toBe calls, and asserts the strongest invariant (full descriptor equality plus non-enumeration). It fails on current Bun (all keys read undefined) and passes with the change, satisfying the USE_SYSTEM_BUN=1 requirement. No CODEOWNERS entry covers src/js/node/http2.ts or the test file. The bug hunt exited on dry_streak with no findings.

@robobun

robobun commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator Author

CI status: the diff is green. The two red items are not from this change.

This PR changes src/js/node/http2.ts only, which cannot affect either.

Node defines 13 entries of http2.constants with NODE_DEFINE_HIDDEN_CONSTANT.
They are read-only and not enumerable, but present. Bun did not define
them, so a comparison against NGHTTP2_ERR_STREAM_CLOSED and friends read
undefined.
@robobun
robobun force-pushed the robobun/ab6b58d0/http2-hidden-constants branch from b4b4bec to 4f7a22f Compare September 6, 2026 11:45

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — small, self-contained Node compat addition.

What was reviewed:

  • Verified all 13 constant values against nghttp2's nghttp2.h enums (HCAT, NV_FLAG, ERR codes) and Node's node_http2.h (STREAM_OPTION_* = 0x1/0x2).
  • Confirmed { value }-only descriptors match NODE_DEFINE_HIDDEN_CONSTANT semantics (non-writable/enumerable/configurable), and that Object.defineProperties at module-load time on a fresh literal is the established pattern here — no tamper-resistance concern.
  • Test asserts exact values, exact descriptors, and non-enumerability; added to the existing http2 test file.
Extended reasoning...

Overview

This PR adds 13 constants to require("node:http2").constants that Node.js defines via NODE_DEFINE_HIDDEN_CONSTANT in src/node_http2.cc — the NGHTTP2_HCAT_*, NGHTTP2_NV_FLAG_*, five NGHTTP2_ERR_* codes, and two STREAM_OPTION_* flags. In Node these are present but non-enumerable/non-writable/non-configurable, so a { value }-only descriptor via Object.defineProperties on the existing constants literal reproduces that exactly. A test in the existing node-http2.test.js file asserts each value, each full property descriptor, and that the keys stay out of Object.keys(constants).

Security risks

None. This is a pure additive constants table on a Node compat module. No user input is parsed, no I/O, no auth/crypto surface. The Object.defineProperties call runs once at module-load time on a literal created three lines above, before any user code can monkey-patch globals, so the "hostile userland" concern for src/js/ builtins does not apply here.

Level of scrutiny

Low. The change is 16 lines of source plus a 34-line test. The only correctness risk is a wrong magic number, so I cross-checked each value: the NGHTTP2_HCAT_* (0-3) and NGHTTP2_NV_FLAG_* (0, 1) values match the nghttp2_headers_category and nghttp2_nv_flag enums in nghttp2; the error codes -501/-508/-509/-510/-901 match NGHTTP2_ERR_INVALID_ARGUMENT/DEFERRED/STREAM_ID_NOT_AVAILABLE/STREAM_CLOSED/NOMEM in nghttp2.h; and STREAM_OPTION_EMPTY_PAYLOAD=1/GET_TRAILERS=2 match kStreamOptionEmptyPayload = 0x1 / kStreamOptionGetTrailers = 0x2 in Node's src/node_http2.h. NODE_DEFINE_HIDDEN_CONSTANT in Node's node_internals.h sets ReadOnly | DontDelete | DontEnum, which is exactly what a bare { value } descriptor produces.

Other factors

No CODEOWNERS entry covers these files. The test follows repo conventions (added to the existing module test file, .toEqual on a whole object for values, exact descriptor assertions). The PR evidence shows the new test fails on main and passes with the fix in both debug+ASAN and release. The destructuring block immediately after the literal (const { NGHTTP2_SESSION_SERVER, ... } = constants) does not reference any of the new keys, so no interaction there. Exit reason was dry_streak. This is exactly the kind of mechanical, verifiable compat fill-in that does not need a human reviewer.

@robobun

robobun commented Sep 6, 2026 •

Copy link
Copy Markdown
Collaborator Author

CI status for 4f7a22f (build 111227): the one red test is not caused by this diff.

http2 server with minimal maxSessionMemory handles multiple requests times out (15 s) on :darwin: any aarch64. The cause is scheduling, not the constants:

I reported the stress test's cold-start sensitivity separately. The other red item, test-crypto-dh-leak.js on x64-asan, fails on main too.

This branch has not been deployed

No deployments
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.

2 participants