@@ -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) {
@@ -978,6 +999,10 @@ void Http2Session::ConsumeHTTP2Data() {
978999 // Even if all bytes were received, a paused stream may delay the
9791000 // nghttp2_on_frame_recv_callback which may have an END_STREAM flag.
9801001 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 ();
9811006 goto done;
9821007 }
9831008
@@ -988,12 +1013,23 @@ void Http2Session::ConsumeHTTP2Data() {
9881013 stream_buf_allocation_.reset ();
9891014 stream_buf_ = uv_buf_init (nullptr , 0 );
9901015
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+
9911028 // Send any data that was queued up while processing the received data.
9921029 if (ret >= 0 && !is_destroyed ()) {
9931030 SendPendingData ();
9941031 }
9951032
996- done:
9971033 if (ret < 0 ) [[unlikely]] {
9981034 Isolate* isolate = env ()->isolate ();
9991035 Debug (this ,
@@ -1420,6 +1456,9 @@ int Http2Session::OnDataChunkReceived(nghttp2_session* handle,
14201456 len -= avail;
14211457 stream->EmitRead (avail, buf);
14221458
1459+ // JS may have destroyed the stream from inside onread; stop delivering.
1460+ if (stream->is_destroyed ()) break ;
1461+
14231462 // If the stream owner (e.g. the JS Http2Stream) wants more data, just
14241463 // tell nghttp2 that all data has been consumed. Otherwise, defer until
14251464 // more data is being requested.
@@ -1977,6 +2016,12 @@ uint8_t Http2Session::SendPendingData() {
19772016 // SendPendingData should not be called recursively.
19782017 if (is_sending ())
19792018 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+
19802025 // This is cleared by ClearOutgoing().
19812026 set_sending ();
19822027
@@ -2387,10 +2432,48 @@ void Http2Stream::Destroy() {
23872432 // Do nothing if this stream instance is already destroyed
23882433 if (is_destroyed ())
23892434 return ;
2390- if (session_->has_pending_rststream (id_))
2391- FlushRstStream ();
2435+
2436+ // Session may already be gone if destroy was deferred across a session
2437+ // teardown.
2438+ if (!session_) {
2439+ set_destroyed ();
2440+ Detach ();
2441+ return ;
2442+ }
2443+
2444+ // Mark destroyed immediately so OnDataChunkReceived stops EmitRead into an
2445+ // already-destroyed JS stream (which would treat the byte count as errno).
23922446 set_destroyed ();
23932447
2448+ // While mem_recv is active, do not FlushRstStream or RemoveStream yet:
2449+ // - FlushRstStream would close the nghttp2 stream before queued response
2450+ // DATA can be mem_send'd after receive returns.
2451+ // - RemoveStream would make OnSendData/Provider::OnRead fail to FindStream.
2452+ // Pending RSTs stay in pending_rst_streams_ and are flushed from
2453+ // ClearOutgoing after the post-receive SendPendingData.
2454+ if (session_->is_receiving ()) {
2455+ BaseObjectPtr<Http2Stream> strong_ref{this };
2456+ env ()->SetImmediate (
2457+ [this , strong_ref](Environment*) { CompleteDestroyCleanup (); });
2458+ return ;
2459+ }
2460+
2461+ if (session_->has_pending_rststream (id_)) FlushRstStream ();
2462+
2463+ CompleteDestroyCleanup ();
2464+ }
2465+
2466+ void Http2Stream::CompleteDestroyCleanup () {
2467+ if (!session_) {
2468+ Detach ();
2469+ return ;
2470+ }
2471+
2472+ // Destroy() always set_destroyed() before scheduling or calling this.
2473+ CHECK (is_destroyed ());
2474+
2475+ if (session_->has_pending_rststream (id_)) FlushRstStream ();
2476+
23942477 Debug (this , " destroying stream" );
23952478
23962479 // Wait until the start of the next loop to delete because there
@@ -2427,7 +2510,6 @@ void Http2Stream::Destroy() {
24272510 EmitStatistics ();
24282511}
24292512
2430-
24312513// Initiates a response on the Http2Stream using data provided via the
24322514// StreamBase Streams API.
24332515int Http2Stream::SubmitResponse (const Http2Headers& headers, int options) {
@@ -2575,8 +2657,8 @@ void Http2Stream::SubmitRstStream(const uint32_t code) {
25752657}
25762658
25772659void Http2Stream::FlushRstStream () {
2578- if (is_destroyed ())
2579- return ;
2660+ if (!session_) return ;
2661+ session_-> RemovePendingRstStream (id_) ;
25802662 Http2Scope h2scope (this );
25812663 CHECK_EQ (nghttp2_submit_rst_stream (
25822664 session_->session (),
0 commit comments