Skip to content

http2: fix graceful session close - #57808

Merged
nodejs-github-bot merged 7 commits into
nodejs:mainfrom
pandeykushagra51:http2/fix-graceful-session-close
Apr 19, 2025
Merged

nodejs-github-bot merged 7 commits into
nodejs:mainfrom
pandeykushagra51:http2/fix-graceful-session-close

Conversation

@pandeykushagra51

@pandeykushagra51 pandeykushagra51 commented Apr 9, 2025 •

Copy link
Copy Markdown
Contributor

Fix issue where session.close() prematurely destroys the session when response.end() was called with an empty payload while active http2 streams still existed. This change ensures that sessions are closed gracefully only after all http2 streams complete and clients properly receive the GOAWAY frame as per the HTTP/2 spec.

Refs: https://nodejs.org/api/http2.html\#http2sessionclosecallback
Fixes: #57809

Update:

Please refer detailed explanation below to know the intention behind updating test/parallel/test-http2-client-rststream-before-connect.js test.
#57808 (comment)

Fix issue where session.close() prematurely destroys the session
when response.end() was called with an empty payload while active
http2 streams still existed. This change ensures that sessions are
closed gracefully only after all http2 streams complete and clients
properly receive the GOAWAY frame as per the HTTP/2 spec.

Refs: https://nodejs.org/api/http2.html\#http2sessionclosecallback
@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++. lib / src Issues and PRs involving general changes in the lib/ or src/ directories. needs-ci PRs that need a full CI run. labels Apr 9, 2025
@pandeykushagra51

pandeykushagra51 commented Apr 9, 2025 •

Copy link
Copy Markdown
Contributor Author

Problem:

When a client calls response.end() with an empty payload, Node.js sends header only frames to nghttp2 as expected, and the JavaScript layer assumes these frames will be successfully transmitted post that. If session.close() is called immediately afterward, the Http2Session class doesn't recognize any active streams (as it only tracks data frames and push promises as active streams), and begins terminating the underlying socket. However, when the C++ layer subsequently attempts to transmit the queued header frames, it fails because the socket has already been closed, resulting in transmission errors and clients never receiving essential GOAWAY frames and response header.

Solution:

The solution ensures proper graceful session closure by delaying socket termination until all data in the nghttp2 queue/buffer is fully processed. This is implemented by:
Adding checks for nghttp2_session_want_write and nghttp2_session_want_read before closing the underlying socket
If nghttp2 has pending data to transmit, postponing the socket closure
Implementing a callback mechanism to notify the JavaScript layer when all writes have completed
Only proceeding with socket closure when all transmission conditions are satisfied
This approach guarantees that all frames, including header-only responses and GOAWAY frames, are properly delivered to the client before connection termination, ensuring robust HTTP/2 protocol compliance.

@pandeykushagra51

Copy link
Copy Markdown
Contributor Author

I will be very happy to update the current solution in case of any scope of improvement and would love to take input to optimise this further.

@pandeykushagra51

pandeykushagra51 commented Apr 9, 2025 •

Copy link
Copy Markdown
Contributor Author

Optimization Proposal for the current fix

Current Issue:

The JavaScript layer is being called after each write operation to underlying socket.
This creates overhead with repetitive calls to onstreamaftrwrite_string()

Proposed Solution

Flag-Based Approach:
Set a flag in the C++ layer when JavaScript initiates a graceful closure
C++ layer checks this flag during write operations.

Responsibility Shift:

When the flag is set, make the C++ layer responsible for notifying the JavaScript layer
Notification happens either:
After nghttp2 completes all read and write operations (i.e hasPendingData return false)

Benefits:

Reduces repetitive calls to onstreamaftrwrite_string()
Improves performance by minimizing call between js and cpp layer.

Trade-off:

Implementation will be more complex
Solution will be longer but more efficient

Comment on lines -3303 to +3306
session[kMaybeDestroy](err);
closeSession(session, NGHTTP2_NO_ERROR, err);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This change is done as improvement. This is because when underlying socket is closed there is no need for looking at graceful closure of session by calling [kMaybeDestroy] instead we can immediately call closeSession which will handle all cleanup operation.

@pandeykushagra51

Copy link
Copy Markdown
Contributor Author

One of the simplest approach is to delay socket destruction to next iteration of event loop i.e call inside setImmediate but I don't think this will address underlying problem

@codecov

codecov Bot commented Apr 10, 2025 •

Copy link
Copy Markdown

Codecov Report

Attention: Patch coverage is 94.11765% with 3 lines in your changes missing coverage. Please review.

Project coverage is 90.23%. Comparing base (67786c1) to head (d54f12c).
Report is 59 commits behind head on main.

Files with missing lines Patch % Lines
src/node_http2.cc 91.89% 0 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #57808      +/-   ##
==========================================
+ Coverage   90.21%   90.23%   +0.02%     
==========================================
  Files         630      630              
  Lines      185524   185737     +213     
  Branches    36387    36414      +27     
==========================================
+ Hits       167378   167609     +231     
+ Misses      11037    11009      -28     
- Partials     7109     7119      +10     
Files with missing lines Coverage Δ
lib/internal/http2/core.js 95.56% <100.00%> (+<0.01%) ⬆️
src/node_http2.h 91.66% <100.00%> (+0.25%) ⬆️
src/node_http2.cc 83.76% <91.89%> (+0.33%) ⬆️

... and 52 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.

@mcollina

Copy link
Copy Markdown
Member

Some tests/linting are failing.

I think the proposed optimization would be useful.

@pandeykushagra51

pandeykushagra51 commented Apr 10, 2025 •

Copy link
Copy Markdown
Contributor Author

Some tests/linting are failing.

I think the proposed optimization would be useful.

Fine, then I will be implementing more optimised solution as stated above.
Also regarding failing test case, I am running the test on MacOS with below system

Darwin Mac.lan 24.3.0 Darwin Kernel Version 24.3.0: Thu Jan  2 20:24:24 PST 2025; root:xnu-11215.81.4~3/RELEASE_ARM64_T6030 arm64

And all tests are passed every time, so I am a bit confused why it is failing on CI.
I will check for rest of the OS and submit report here.

…allbacks between C++ and JavaScript layers. Added graceful_close_initiated_ flag which will ensure that JS layer will be notified only when session close is initiated and centralized notification logic in CheckAndNotifyJSAfterWrite() to only notify JavaScript when there's no pending data (nghttp2_session_want_write/read return 0). Previously, excessive callbac
@pandeykushagra51

Copy link
Copy Markdown
Contributor Author

Some tests/linting are failing.

I think the proposed optimization would be useful.

hey @mcollina I have implemented following strategy for optimisation:

  1. Introduced a graceful session closed flag in cpp layer which will ensure that the callback will only be called if session close is initiated by js layer.
  2. Added logic to call js layer only if nghttp2_wants_read and nghttp2_wants_write is false

Both of the above things when combined, will ensure that the new callback onstreamafterwrite_string will only be called once during the whole lifecycle of a session.

Regarding Failing tests, I am able to reproduce this occasionally i.e once in 10 run but not able to find root cause yet. I will be investigating further about RC.

@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 requested review from jasnell and pimterry April 11, 2025 07:34
@mcollina mcollina added the request-ci Add this label to start a Jenkins CI on a PR. Only starts once the PR has an approving review. label Apr 11, 2025
@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. Only starts once the PR has an approving review. label Apr 11, 2025
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@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. Only starts once the PR has an approving review. label Apr 11, 2025
@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. Only starts once the PR has an approving review. label Apr 11, 2025
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@pandeykushagra51

Copy link
Copy Markdown
Contributor Author

Hey guys could you please check the last commit-queue.

Add commit-queue-squash label to land the PR as one commit, or commit-queue-rebase to land as separate commits.

@lpinca lpinca added commit-queue PRs queued for automated landing through the Commit Queue. commit-queue-squash PRs the Commit Queue should land as one squashed commit. and removed commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. labels Apr 19, 2025
@nodejs-github-bot nodejs-github-bot removed the commit-queue PRs queued for automated landing through the Commit Queue. label Apr 19, 2025
@nodejs-github-bot
nodejs-github-bot merged commit 2acc8bc into nodejs:main Apr 19, 2025
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Landed in 2acc8bc

@pandeykushagra51

Copy link
Copy Markdown
Contributor Author

Thanks a lot @lpinca @jasnell @mcollina @pimterry.
It was really great working on node. Hope to have some more impactful contribution in near future.

@pandeykushagra51

Copy link
Copy Markdown
Contributor Author

Also please take a look at #57586.
it is also having commit-queue-failed status but have all checks passed and 3 approval.

RafaelGSS pushed a commit that referenced this pull request May 1, 2025
Fix issue where session.close() prematurely destroys the session
when response.end() was called with an empty payload while active
http2 streams still existed. This change ensures that sessions are
closed gracefully only after all http2 streams complete and clients
properly receive the GOAWAY frame as per the HTTP/2 spec.

Refs: https://nodejs.org/api/http2.html\#http2sessionclosecallback
PR-URL: #57808
Fixes: #57809
Refs: https://nodejs.org/api/http2.html%5C#http2sessionclosecallback
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Tim Perry <pimterry@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
RafaelGSS pushed a commit that referenced this pull request May 2, 2025
Fix issue where session.close() prematurely destroys the session
when response.end() was called with an empty payload while active
http2 streams still existed. This change ensures that sessions are
closed gracefully only after all http2 streams complete and clients
properly receive the GOAWAY frame as per the HTTP/2 spec.

Refs: https://nodejs.org/api/http2.html\#http2sessionclosecallback
PR-URL: #57808
Fixes: #57809
Refs: https://nodejs.org/api/http2.html%5C#http2sessionclosecallback
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Tim Perry <pimterry@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
aduh95 pushed a commit that referenced this pull request May 6, 2025
Fix issue where session.close() prematurely destroys the session
when response.end() was called with an empty payload while active
http2 streams still existed. This change ensures that sessions are
closed gracefully only after all http2 streams complete and clients
properly receive the GOAWAY frame as per the HTTP/2 spec.

Refs: https://nodejs.org/api/http2.html\#http2sessionclosecallback
PR-URL: #57808
Fixes: #57809
Refs: https://nodejs.org/api/http2.html%5C#http2sessionclosecallback
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Tim Perry <pimterry@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
aduh95 pushed a commit that referenced this pull request May 6, 2025
Fix issue where session.close() prematurely destroys the session
when response.end() was called with an empty payload while active
http2 streams still existed. This change ensures that sessions are
closed gracefully only after all http2 streams complete and clients
properly receive the GOAWAY frame as per the HTTP/2 spec.

Refs: https://nodejs.org/api/http2.html\#http2sessionclosecallback
PR-URL: #57808
Fixes: #57809
Refs: https://nodejs.org/api/http2.html%5C#http2sessionclosecallback
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Tim Perry <pimterry@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
RafaelGSS pushed a commit that referenced this pull request May 14, 2025
Fix issue where session.close() prematurely destroys the session
when response.end() was called with an empty payload while active
http2 streams still existed. This change ensures that sessions are
closed gracefully only after all http2 streams complete and clients
properly receive the GOAWAY frame as per the HTTP/2 spec.

Refs: https://nodejs.org/api/http2.html\#http2sessionclosecallback
PR-URL: #57808
Fixes: #57809
Refs: https://nodejs.org/api/http2.html%5C#http2sessionclosecallback
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Tim Perry <pimterry@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
aduh95 pushed a commit that referenced this pull request May 16, 2025
Fix issue where session.close() prematurely destroys the session
when response.end() was called with an empty payload while active
http2 streams still existed. This change ensures that sessions are
closed gracefully only after all http2 streams complete and clients
properly receive the GOAWAY frame as per the HTTP/2 spec.

Refs: https://nodejs.org/api/http2.html\#http2sessionclosecallback
PR-URL: #57808
Fixes: #57809
Refs: https://nodejs.org/api/http2.html%5C#http2sessionclosecallback
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Tim Perry <pimterry@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
aduh95 pushed a commit that referenced this pull request May 17, 2025
Fix issue where session.close() prematurely destroys the session
when response.end() was called with an empty payload while active
http2 streams still existed. This change ensures that sessions are
closed gracefully only after all http2 streams complete and clients
properly receive the GOAWAY frame as per the HTTP/2 spec.

Refs: https://nodejs.org/api/http2.html\#http2sessionclosecallback
PR-URL: #57808
Fixes: #57809
Refs: https://nodejs.org/api/http2.html%5C#http2sessionclosecallback
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Tim Perry <pimterry@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
aduh95 pushed a commit that referenced this pull request May 18, 2025
Fix issue where session.close() prematurely destroys the session
when response.end() was called with an empty payload while active
http2 streams still existed. This change ensures that sessions are
closed gracefully only after all http2 streams complete and clients
properly receive the GOAWAY frame as per the HTTP/2 spec.

Refs: https://nodejs.org/api/http2.html\#http2sessionclosecallback
PR-URL: #57808
Fixes: #57809
Refs: https://nodejs.org/api/http2.html%5C#http2sessionclosecallback
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Tim Perry <pimterry@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
aduh95 pushed a commit that referenced this pull request May 19, 2025
Fix issue where session.close() prematurely destroys the session
when response.end() was called with an empty payload while active
http2 streams still existed. This change ensures that sessions are
closed gracefully only after all http2 streams complete and clients
properly receive the GOAWAY frame as per the HTTP/2 spec.

Refs: https://nodejs.org/api/http2.html\#http2sessionclosecallback
PR-URL: #57808
Fixes: #57809
Refs: https://nodejs.org/api/http2.html%5C#http2sessionclosecallback
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Tim Perry <pimterry@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
marco-ippolito pushed a commit that referenced this pull request Jun 5, 2025
Fix issue where session.close() prematurely destroys the session
when response.end() was called with an empty payload while active
http2 streams still existed. This change ensures that sessions are
closed gracefully only after all http2 streams complete and clients
properly receive the GOAWAY frame as per the HTTP/2 spec.

Refs: https://nodejs.org/api/http2.html\#http2sessionclosecallback
PR-URL: #57808
Fixes: #57809
Refs: https://nodejs.org/api/http2.html%5C#http2sessionclosecallback
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Tim Perry <pimterry@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
marco-ippolito pushed a commit that referenced this pull request Jun 5, 2025
Fix issue where session.close() prematurely destroys the session
when response.end() was called with an empty payload while active
http2 streams still existed. This change ensures that sessions are
closed gracefully only after all http2 streams complete and clients
properly receive the GOAWAY frame as per the HTTP/2 spec.

Refs: https://nodejs.org/api/http2.html\#http2sessionclosecallback
PR-URL: #57808
Fixes: #57809
Refs: https://nodejs.org/api/http2.html%5C#http2sessionclosecallback
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Tim Perry <pimterry@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
@ghost ghost mentioned this pull request Jun 8, 2025
marco-ippolito pushed a commit that referenced this pull request Jun 10, 2025
Fix issue where session.close() prematurely destroys the session
when response.end() was called with an empty payload while active
http2 streams still existed. This change ensures that sessions are
closed gracefully only after all http2 streams complete and clients
properly receive the GOAWAY frame as per the HTTP/2 spec.

Refs: https://nodejs.org/api/http2.html\#http2sessionclosecallback
PR-URL: #57808
Fixes: #57809
Refs: https://nodejs.org/api/http2.html%5C#http2sessionclosecallback
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Tim Perry <pimterry@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
marco-ippolito pushed a commit that referenced this pull request Jun 11, 2025
Fix issue where session.close() prematurely destroys the session
when response.end() was called with an empty payload while active
http2 streams still existed. This change ensures that sessions are
closed gracefully only after all http2 streams complete and clients
properly receive the GOAWAY frame as per the HTTP/2 spec.

Refs: https://nodejs.org/api/http2.html\#http2sessionclosecallback
PR-URL: #57808
Fixes: #57809
Refs: https://nodejs.org/api/http2.html%5C#http2sessionclosecallback
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Tim Perry <pimterry@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author ready PRs with CI started, the required approvals, and no outstanding review comments. c++ Issues and PRs that require attention from people who are familiar with C++. commit-queue-squash PRs the Commit Queue should land as one squashed commit. lib / src Issues and PRs involving general changes in the lib/ or src/ directories. needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

graceful http2 session closure with empty body

6 participants