Skip to content

fix(ext/node): emit ERR_HTTP2_TOO_MANY_INVALID_FRAMES for empty DATA frames without END_STREAM - #33644

Merged
littledivy merged 1 commit into
denoland:mainfrom
divybot:claude/test-http2-empty-frame-without-eof
Apr 29, 2026
Merged

littledivy merged 1 commit into
denoland:mainfrom
divybot:claude/test-http2-empty-frame-without-eof

Conversation

@divybot

@divybot divybot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Enables test-http2-empty-frame-without-eof in node_compat suite.

Test plan

  • cargo test --test node_compat -- test-http2-empty-frame-without-eof

…frames without END_STREAM

Enables tests/node_compat/runner/suite/test/parallel/test-http2-empty-frame-without-eof.js

Co-authored-by: Divy Srivastava <me@littledivy.com>
@divybot
divybot force-pushed the claude/test-http2-empty-frame-without-eof branch from 4a179e5 to d845e89 Compare April 28, 2026 17:01

@fibibot fibibot 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. Walked through the substance:

  1. Empty DATA frame detection in on_frame_recv_callback: when frame_header_length(frame) == 0 and END_STREAM is unset, increment invalid_frame_count and trip if it exceeds max_invalid_frames. nghttp2 itself doesn't flag empty-DATA-without-EOF as a protocol error, so the JS layer needs to count it β€” same as Node's HandleDataFrame in src/node_http2.cc.

  2. Post-increment comparison corrected in on_invalid_frame_recv_callback: changed from count >= session.max_invalid_frames (with a > 0 guard that disabled max=0) to count > session.max_invalid_frames, matching Node's if (invalid_frame_count_++ > max_invalid_frames). The semantics now line up exactly:

    • max=0: 1 invalid frame allowed (count=0, check 0 > 0 false), 2nd trips (count=1, 1 > 0 true).
    • max=2: 2 allowed, 3rd trips.
    • That's exactly what the upstream test exercises (max=0 β†’ must error with "Too many invalid HTTP/2 frames"; max=2 β†’ must not error).
  3. custom_recv_error_code plumbing: nghttp2 callbacks set Some("ERR_HTTP2_TOO_MANY_INVALID_FRAMES"), then receive_data checks the return value of nghttp2_session_mem_recv β€” if negative (fatal), emit_session_internal_error(ret) builds a v8 scope and invokes session_internal_error_cb(integerCode, customErrorCode). Mirrors Node's Http2Session::custom_recv_error_code_ exactly. βœ“

  4. emit_session_internal_error uses the same v8-scope pattern (from_raw_isolate_ptr β†’ ContextScope β†’ callback.call) as the existing invoke_session_internal_error helper. The custom code is take()n out of the field so subsequent emits don't re-emit it. Note this has 2 args (errno, customErrorCode) where the existing invoke_session_internal_error has 1 (errno only) β€” that's intentional, since this is the path that needs to surface the custom code to JS.

  5. JS-side kCustomErrorMessages map translates ERR_HTTP2_TOO_MANY_INVALID_FRAMES to "Too many invalid HTTP/2 frames". __proto__: null prevents pollution. Falls back to the code string if unknown. Matches the test's expectsError({code: 'ERR_HTTP2_TOO_MANY_INVALID_FRAMES', message: 'Too many invalid HTTP/2 frames'}). βœ“

  6. saturating_add on invalid_frame_count: u32 defends against overflow at the cost of saturating at u32::MAX. Reasonable β€” by the time you've received 4B invalid frames, the session is well past the limit.

Test enrollment alphabetically positioned (dont-override < empty-frame-without-eof < endafterheaders). βœ“

CI fully green: 133 SUCCESS, 0 FAILURE, 2 SKIPPED. All 36 test node_compat shards Γ— all 12 test unit_node shards Γ— all 24 test specs shards green.

@littledivy
littledivy merged commit c12536c into denoland:main Apr 29, 2026
136 checks passed
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.

3 participants