@@ -820,6 +820,21 @@ void Http2Session::Close(uint32_t code, bool socket_closed) {
820820 return ;
821821 set_closing ();
822822
823+ // Do not flush GOAWAY from inside nghttp2_session_mem_recv() callbacks.
824+ // ConsumeHTTP2Data() finishes the close once mem_recv returns.
825+ if (is_receiving ()) {
826+ set_close_pending ();
827+ pending_close_code_ = code;
828+ pending_close_socket_closed_ = socket_closed;
829+ return ;
830+ }
831+
832+ FinishClose (code, socket_closed);
833+ }
834+
835+ void Http2Session::FinishClose (uint32_t code, bool socket_closed) {
836+ CHECK (is_closing ());
837+
823838 // Stop reading on the i/o stream
824839 if (stream_ != nullptr ) {
825840 set_reading_stopped ();
@@ -869,6 +884,12 @@ void Http2Session::Close(uint32_t code, bool socket_closed) {
869884 EmitStatistics ();
870885}
871886
887+ void Http2Session::MaybeFinishPendingClose () {
888+ if (!is_close_pending () || is_destroyed ()) return ;
889+ set_close_pending (false );
890+ FinishClose (pending_close_code_, pending_close_socket_closed_);
891+ }
892+
872893// Locates an existing known stream by ID. nghttp2 has a similar method
873894// but this is faster and does not fail if the stream is not found.
874895BaseObjectPtr<Http2Stream> Http2Session::FindStream (int32_t id) {
@@ -963,11 +984,13 @@ void Http2Session::ConsumeHTTP2Data() {
963984 nghttp2_session_want_read (session_.get ()));
964985 set_receive_paused (false );
965986 custom_recv_error_code_ = nullptr ;
987+ set_receiving ();
966988 ssize_t ret =
967989 nghttp2_session_mem_recv (session_.get (),
968990 reinterpret_cast <uint8_t *>(stream_buf_.base ) +
969991 stream_buf_offset_,
970992 read_len);
993+ set_receiving (false );
971994 CHECK_NE (ret, NGHTTP2_ERR_NOMEM );
972995 CHECK_IMPLIES (custom_recv_error_code_ != nullptr , ret < 0 );
973996
@@ -981,6 +1004,10 @@ void Http2Session::ConsumeHTTP2Data() {
9811004 // Even if all bytes were received, a paused stream may delay the
9821005 // nghttp2_on_frame_recv_callback which may have an END_STREAM flag.
9831006 stream_buf_offset_ += ret;
1007+ // Still complete a Close() deferred during mem_recv; do not fall through
1008+ // to SendPendingData() here (paused receives historically skip that flush
1009+ // because a write may already be in progress).
1010+ MaybeFinishPendingClose ();
9841011 goto done;
9851012 }
9861013
@@ -991,12 +1018,23 @@ void Http2Session::ConsumeHTTP2Data() {
9911018 stream_buf_allocation_.reset ();
9921019 stream_buf_ = uv_buf_init (nullptr , 0 );
9931020
1021+ // Finish a Close() deferred during mem_recv before flushing, so GOAWAY is
1022+ // not written after pending RST_STREAM frames.
1023+ MaybeFinishPendingClose ();
1024+
1025+ done:
1026+ // Finish a Close() deferred above before flushing, so GOAWAY is not written
1027+ // after pending RST_STREAM frames.
1028+ if (is_close_pending () && !is_destroyed ()) {
1029+ set_close_pending (false );
1030+ FinishClose (pending_close_code_, pending_close_socket_closed_);
1031+ }
1032+
9941033 // Send any data that was queued up while processing the received data.
9951034 if (ret >= 0 && !is_destroyed ()) {
9961035 SendPendingData ();
9971036 }
9981037
999- done:
10001038 if (ret < 0 ) [[unlikely]] {
10011039 Isolate* isolate = env ()->isolate ();
10021040 Debug (this ,
@@ -1410,6 +1448,9 @@ int Http2Session::OnDataChunkReceived(nghttp2_session* handle,
14101448 len -= avail;
14111449 stream->EmitRead (avail, buf);
14121450
1451+ // JS may have destroyed the stream from inside onread; stop delivering.
1452+ if (stream->is_destroyed ()) break ;
1453+
14131454 // If the stream owner (e.g. the JS Http2Stream) wants more data, just
14141455 // tell nghttp2 that all data has been consumed. Otherwise, defer until
14151456 // more data is being requested.
@@ -1967,6 +2008,12 @@ uint8_t Http2Session::SendPendingData() {
19672008 // SendPendingData should not be called recursively.
19682009 if (is_sending ())
19692010 return 1 ;
2011+
2012+ // Do not call `nghttp2_session_mem_send()` while nghttp2 is processing
2013+ // incoming data. Sending may close the stream and free nghttp2 state
2014+ // that is still in use by `nghttp2_session_mem_recv()`.
2015+ if (is_receiving ()) return 1 ;
2016+
19702017 // This is cleared by ClearOutgoing().
19712018 set_sending ();
19722019
@@ -2376,10 +2423,48 @@ void Http2Stream::Destroy() {
23762423 // Do nothing if this stream instance is already destroyed
23772424 if (is_destroyed ())
23782425 return ;
2379- if (session_->has_pending_rststream (id_))
2380- FlushRstStream ();
2426+
2427+ // Session may already be gone if destroy was deferred across a session
2428+ // teardown.
2429+ if (!session_) {
2430+ set_destroyed ();
2431+ Detach ();
2432+ return ;
2433+ }
2434+
2435+ // Mark destroyed immediately so OnDataChunkReceived stops EmitRead into an
2436+ // already-destroyed JS stream (which would treat the byte count as errno).
23812437 set_destroyed ();
23822438
2439+ // While mem_recv is active, do not FlushRstStream or RemoveStream yet:
2440+ // - FlushRstStream would close the nghttp2 stream before queued response
2441+ // DATA can be mem_send'd after receive returns.
2442+ // - RemoveStream would make OnSendData/Provider::OnRead fail to FindStream.
2443+ // Pending RSTs stay in pending_rst_streams_ and are flushed from
2444+ // ClearOutgoing after the post-receive SendPendingData.
2445+ if (session_->is_receiving ()) {
2446+ BaseObjectPtr<Http2Stream> strong_ref{this };
2447+ env ()->SetImmediate (
2448+ [this , strong_ref](Environment*) { CompleteDestroyCleanup (); });
2449+ return ;
2450+ }
2451+
2452+ if (session_->has_pending_rststream (id_)) FlushRstStream ();
2453+
2454+ CompleteDestroyCleanup ();
2455+ }
2456+
2457+ void Http2Stream::CompleteDestroyCleanup () {
2458+ if (!session_) {
2459+ Detach ();
2460+ return ;
2461+ }
2462+
2463+ // Destroy() always set_destroyed() before scheduling or calling this.
2464+ CHECK (is_destroyed ());
2465+
2466+ if (session_->has_pending_rststream (id_)) FlushRstStream ();
2467+
23832468 Debug (this , " destroying stream" );
23842469
23852470 // Wait until the start of the next loop to delete because there
@@ -2416,7 +2501,6 @@ void Http2Stream::Destroy() {
24162501 EmitStatistics ();
24172502}
24182503
2419-
24202504// Initiates a response on the Http2Stream using data provided via the
24212505// StreamBase Streams API.
24222506int Http2Stream::SubmitResponse (const Http2Headers& headers, int options) {
@@ -2525,6 +2609,18 @@ void Http2Stream::SubmitRstStream(const uint32_t code) {
25252609 return code == NGHTTP2_CANCEL ;
25262610 };
25272611
2612+ // Do not call `nghttp2_session_mem_send()` while nghttp2 is processing
2613+ // incoming data. Sending may close the stream and free nghttp2 state
2614+ // that is still in use by `nghttp2_session_mem_recv()`.
2615+ if (session_->is_receiving () && available_outbound_length_ == 0 ) {
2616+ if (is_stream_cancel (code)) {
2617+ session_->AddPendingRstStream (id_);
2618+ return ;
2619+ }
2620+ FlushRstStream ();
2621+ return ;
2622+ }
2623+
25282624 // If RST_STREAM frame is received with error code NGHTTP2_CANCEL,
25292625 // add it to the pending list and don't force purge the data. It is
25302626 // to avoids the double free error due to unwanted behavior of nghttp2.
@@ -2560,8 +2656,8 @@ void Http2Stream::SubmitRstStream(const uint32_t code) {
25602656}
25612657
25622658void Http2Stream::FlushRstStream () {
2563- if (is_destroyed ())
2564- return ;
2659+ if (!session_) return ;
2660+ session_-> RemovePendingRstStream (id_) ;
25652661 Http2Scope h2scope (this );
25662662 CHECK_EQ (nghttp2_submit_rst_stream (
25672663 session_->session (),
0 commit comments