@@ -815,6 +815,21 @@ void Http2Session::Close(uint32_t code, bool socket_closed) {
815815 return ;
816816 set_closing ();
817817
818+ // Do not flush GOAWAY from inside nghttp2_session_mem_recv() callbacks.
819+ // ConsumeHTTP2Data() finishes the close once mem_recv returns.
820+ if (is_receiving ()) {
821+ set_close_pending ();
822+ pending_close_code_ = code;
823+ pending_close_socket_closed_ = socket_closed;
824+ return ;
825+ }
826+
827+ FinishClose (code, socket_closed);
828+ }
829+
830+ void Http2Session::FinishClose (uint32_t code, bool socket_closed) {
831+ CHECK (is_closing ());
832+
818833 // Stop reading on the i/o stream
819834 if (stream_ != nullptr ) {
820835 set_reading_stopped ();
@@ -864,6 +879,12 @@ void Http2Session::Close(uint32_t code, bool socket_closed) {
864879 EmitStatistics ();
865880}
866881
882+ void Http2Session::MaybeFinishPendingClose () {
883+ if (!is_close_pending () || is_destroyed ()) return ;
884+ set_close_pending (false );
885+ FinishClose (pending_close_code_, pending_close_socket_closed_);
886+ }
887+
867888// Locates an existing known stream by ID. nghttp2 has a similar method
868889// but this is faster and does not fail if the stream is not found.
869890BaseObjectPtr<Http2Stream> Http2Session::FindStream (int32_t id) {
@@ -958,11 +979,13 @@ void Http2Session::ConsumeHTTP2Data() {
958979 nghttp2_session_want_read (session_.get ()));
959980 set_receive_paused (false );
960981 custom_recv_error_code_ = nullptr ;
982+ set_receiving ();
961983 ssize_t ret =
962984 nghttp2_session_mem_recv (session_.get (),
963985 reinterpret_cast <uint8_t *>(stream_buf_.base ) +
964986 stream_buf_offset_,
965987 read_len);
988+ set_receiving (false );
966989 CHECK_NE (ret, NGHTTP2_ERR_NOMEM );
967990 CHECK_IMPLIES (custom_recv_error_code_ != nullptr , ret < 0 );
968991
@@ -976,6 +999,10 @@ void Http2Session::ConsumeHTTP2Data() {
976999 // Even if all bytes were received, a paused stream may delay the
9771000 // nghttp2_on_frame_recv_callback which may have an END_STREAM flag.
9781001 stream_buf_offset_ += ret;
1002+ // Still complete a Close() deferred during mem_recv; do not fall through
1003+ // to SendPendingData() here (paused receives historically skip that flush
1004+ // because a write may already be in progress).
1005+ MaybeFinishPendingClose ();
9791006 goto done;
9801007 }
9811008
@@ -986,12 +1013,23 @@ void Http2Session::ConsumeHTTP2Data() {
9861013 stream_buf_allocation_.reset ();
9871014 stream_buf_ = uv_buf_init (nullptr , 0 );
9881015
1016+ // Finish a Close() deferred during mem_recv before flushing, so GOAWAY is
1017+ // not written after pending RST_STREAM frames.
1018+ MaybeFinishPendingClose ();
1019+
1020+ done:
1021+ // Finish a Close() deferred above before flushing, so GOAWAY is not written
1022+ // after pending RST_STREAM frames.
1023+ if (is_close_pending () && !is_destroyed ()) {
1024+ set_close_pending (false );
1025+ FinishClose (pending_close_code_, pending_close_socket_closed_);
1026+ }
1027+
9891028 // Send any data that was queued up while processing the received data.
9901029 if (ret >= 0 && !is_destroyed ()) {
9911030 SendPendingData ();
9921031 }
9931032
994- done:
9951033 if (ret < 0 ) [[unlikely]] {
9961034 Isolate* isolate = env ()->isolate ();
9971035 Debug (this ,
@@ -1418,6 +1456,9 @@ int Http2Session::OnDataChunkReceived(nghttp2_session* handle,
14181456 len -= avail;
14191457 stream->EmitRead (avail, buf);
14201458
1459+ // JS may have destroyed the stream from inside onread; stop delivering.
1460+ if (stream->is_destroyed ()) break ;
1461+
14211462 // If the stream owner (e.g. the JS Http2Stream) wants more data, just
14221463 // tell nghttp2 that all data has been consumed. Otherwise, defer until
14231464 // more data is being requested.
@@ -1975,6 +2016,12 @@ uint8_t Http2Session::SendPendingData() {
19752016 // SendPendingData should not be called recursively.
19762017 if (is_sending ())
19772018 return 1 ;
2019+
2020+ // Do not call `nghttp2_session_mem_send()` while nghttp2 is processing
2021+ // incoming data. Sending may close the stream and free nghttp2 state
2022+ // that is still in use by `nghttp2_session_mem_recv()`.
2023+ if (is_receiving ()) return 1 ;
2024+
19782025 // This is cleared by ClearOutgoing().
19792026 set_sending ();
19802027
@@ -2387,10 +2434,48 @@ void Http2Stream::Destroy() {
23872434 // Do nothing if this stream instance is already destroyed
23882435 if (is_destroyed ())
23892436 return ;
2390- if (session_->has_pending_rststream (id_))
2391- FlushRstStream ();
2437+
2438+ // Session may already be gone if destroy was deferred across a session
2439+ // teardown.
2440+ if (!session_) {
2441+ set_destroyed ();
2442+ Detach ();
2443+ return ;
2444+ }
2445+
2446+ // Mark destroyed immediately so OnDataChunkReceived stops EmitRead into an
2447+ // already-destroyed JS stream (which would treat the byte count as errno).
23922448 set_destroyed ();
23932449
2450+ // While mem_recv is active, do not FlushRstStream or RemoveStream yet:
2451+ // - FlushRstStream would close the nghttp2 stream before queued response
2452+ // DATA can be mem_send'd after receive returns.
2453+ // - RemoveStream would make OnSendData/Provider::OnRead fail to FindStream.
2454+ // Pending RSTs stay in pending_rst_streams_ and are flushed from
2455+ // ClearOutgoing after the post-receive SendPendingData.
2456+ if (session_->is_receiving ()) {
2457+ BaseObjectPtr<Http2Stream> strong_ref{this };
2458+ env ()->SetImmediate (
2459+ [this , strong_ref](Environment*) { CompleteDestroyCleanup (); });
2460+ return ;
2461+ }
2462+
2463+ if (session_->has_pending_rststream (id_)) FlushRstStream ();
2464+
2465+ CompleteDestroyCleanup ();
2466+ }
2467+
2468+ void Http2Stream::CompleteDestroyCleanup () {
2469+ if (!session_) {
2470+ Detach ();
2471+ return ;
2472+ }
2473+
2474+ // Destroy() always set_destroyed() before scheduling or calling this.
2475+ CHECK (is_destroyed ());
2476+
2477+ if (session_->has_pending_rststream (id_)) FlushRstStream ();
2478+
23942479 Debug (this , " destroying stream" );
23952480
23962481 // Wait until the start of the next loop to delete because there
@@ -2427,7 +2512,6 @@ void Http2Stream::Destroy() {
24272512 EmitStatistics ();
24282513}
24292514
2430-
24312515// Initiates a response on the Http2Stream using data provided via the
24322516// StreamBase Streams API.
24332517int Http2Stream::SubmitResponse (const Http2Headers& headers, int options) {
@@ -2559,6 +2643,18 @@ void Http2Stream::SubmitRstStream(const uint32_t code) {
25592643 return code == NGHTTP2_CANCEL ;
25602644 };
25612645
2646+ // Do not call `nghttp2_session_mem_send()` while nghttp2 is processing
2647+ // incoming data. Sending may close the stream and free nghttp2 state
2648+ // that is still in use by `nghttp2_session_mem_recv()`.
2649+ if (session_->is_receiving () && available_outbound_length_ == 0 ) {
2650+ if (is_stream_cancel (code)) {
2651+ session_->AddPendingRstStream (id_);
2652+ return ;
2653+ }
2654+ FlushRstStream ();
2655+ return ;
2656+ }
2657+
25622658 // If RST_STREAM frame is received with error code NGHTTP2_CANCEL,
25632659 // add it to the pending list and don't force purge the data. It is
25642660 // to avoids the double free error due to unwanted behavior of nghttp2.
@@ -2586,8 +2682,8 @@ void Http2Stream::SubmitRstStream(const uint32_t code) {
25862682}
25872683
25882684void Http2Stream::FlushRstStream () {
2589- if (is_destroyed ())
2590- return ;
2685+ if (!session_) return ;
2686+ session_-> RemovePendingRstStream (id_) ;
25912687 Http2Scope h2scope (this );
25922688 CHECK_EQ (nghttp2_submit_rst_stream (
25932689 session_->session (),
0 commit comments