Skip to content

http2: fix onread assert when destroying session from stream handler - #65116

Open
sankalpsthakur wants to merge 3 commits into
nodejs:mainfrom
sankalpsthakur:fix/64850-http2-onread-assert
Open

http2: fix onread assert when destroying session from stream handler#65116
sankalpsthakur wants to merge 3 commits into
nodejs:mainfrom
sankalpsthakur:fix/64850-http2-onread-assert

Conversation

@sankalpsthakur

@sankalpsthakur sankalpsthakur commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

When session.destroy() is called from a 'stream' handler, MakeCallback drains nextTick while nghttp2 is still inside mem_recv. Session close is deferred for that window (from #64166), so later HEADERS in the same receive buffer created C++ streams without a JS wrapper / onread. Subsequent DATA delivery hit:

Assertion failed: onread->IsFunction()
  <- StreamBase::CallJSOnreadMethod
  <- Http2StreamListener::OnStreamRead
  <- Http2Session::OnDataChunkReceived

Changes

  1. Do not create streams while the session is closing (OnBeginHeadersCallback) — RST with NGHTTP2_CANCEL instead.
  2. If on_headers runs after JS destroy, destroy the C++ handle (mirrors the existing session.closed path).
  3. Defensive: drop DATA when onread is not a function rather than aborting.
  4. Regression test exercising concurrent POSTs with session.destroy() from the stream handler.

Test plan

  • New test/parallel/test-http2-session-destroy-stream-handler.js (40 rounds × 8 concurrent POSTs)

Fixes #64850

AI/LLM disclosure

  • AI coding tools (including Grok and/or Codex agent-assisted editing) were used to help draft or modify code and this PR description.
  • I reviewed the complete change, understand the reasoning, and added the regression test listed above.
  • This submission is original work of authorship under the project CLA / contributor terms; AI output was not pasted unreviewed.

When session.destroy() runs from a 'stream' handler, MakeCallback drains
nextTick while nghttp2 is still inside mem_recv. Close is deferred for
that window (see nodejs#64166), so later HEADERS in the same buffer created
C++ streams without a JS wrapper or onread, and DATA delivery aborted
with Assertion failed: onread->IsFunction().

- Reject new streams while the session is closing
- Destroy the C++ handle if on_headers runs after JS destroy
- Drop DATA when onread is not installed (defensive)

Fixes: nodejs#64850
Signed-off-by: Sankalp Thakur <sankalphimself@gmail.com>
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/http2
  • @nodejs/net

@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. http2 Issues and PRs related to the http2 subsystem. needs-ci PRs that need a full CI run. labels Aug 7, 2026

@mcollina mcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm

@mcollina mcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 31, 2026
@mcollina
mcollina requested a review from pimterry August 31, 2026 09:44
@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 31, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@codecov

codecov Bot commented Aug 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 63.63636% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.05%. Comparing base (e2d7b34) to head (ae6f128).
⚠️ Report is 422 commits behind head on main.

Files with missing lines Patch % Lines
lib/internal/http2/core.js 50.00% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #65116      +/-   ##
==========================================
- Coverage   90.31%   90.05%   -0.27%     
==========================================
  Files         759      754       -5     
  Lines      248290   256406    +8116     
  Branches    46859    48500    +1641     
==========================================
+ Hits       224241   230895    +6654     
- Misses      15472    16620    +1148     
- Partials     8577     8891     +314     
Files with missing lines Coverage Δ
src/node_http2.cc 81.96% <100.00%> (+0.21%) ⬆️
lib/internal/http2/core.js 94.99% <50.00%> (-0.09%) ⬇️

... and 269 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@pimterry pimterry left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks @sankalpsthakur!

Mostly looks good. The 3 fixes seem to all separately solve the same issue redundantly, is that right? I think for the first two that's OK and they're independently valuable (with minor tweaks) but we should drop the 3rd chunk to avoid missing major issues in future.

There's also a failing lint here, you can fix this locally with make format-cpp.

Comment thread src/node_http2.cc Outdated
session->session(),
NGHTTP2_FLAG_NONE,
id,
NGHTTP2_CANCEL);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this should be NGHTTP2_REFUSED_STREAM (which tells the client that we didn't even start processing, and so it can safely retry) rather than CANCEL which just says we didn't finish.

Comment thread src/node_http2.cc Outdated
->GetInternalField(StreamBase::kOnReadFunctionField)
.As<Value>();
if (!onread->IsFunction())
return;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The other 2 fixes handle a specific valid shutdown scenario and cleanly make sure we don't start new work.

This fix though covers a generic broken state by simply ignoring it and dropping data.

We shouldn't do this. With this other fixes this is now unreachable, but if there's any other future scenario where we end up with a broken read callback state here somehow, we want to know about it rather than losing data silently.

// still walking the receive buffer). Tear down the C++ stream so subsequent
// DATA frames do not call CallJSOnreadMethod with a missing onread.
if (session.destroyed) {
handle.destroy();

@pimterry pimterry Aug 31, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: This should send REFUSED_STREAM too I think, matching the session.closed below that it's mirroring.

Nit because really this code is just defense in depth, and the 2nd chunk here makes it unreachable, but if we ever did reach it somehow, we would want to tell the other peer we haven't processed their stream so they can retry it, rather than silently disappearing.

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

Labels

c++ Issues and PRs that require attention from people who are familiar with C++. http2 Issues and PRs related to the http2 subsystem. needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

http2: assertion failure onread->IsFunction() when a session is destroyed from a 'stream' handler

4 participants