@@ -815,6 +815,21 @@ void Http2Session::Close(uint32_t code, bool socket_closed) { |
815 | 815 | return; |
816 | 816 | set_closing(); |
817 | 817 |
|
| 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 | + |
818 | 833 | // Stop reading on the i/o stream |
819 | 834 | if (stream_ != nullptr) { |
820 | 835 | set_reading_stopped(); |
@@ -864,6 +879,12 @@ void Http2Session::Close(uint32_t code, bool socket_closed) { |
864 | 879 | EmitStatistics(); |
865 | 880 | } |
866 | 881 |
|
| 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 | + |
867 | 888 | // Locates an existing known stream by ID. nghttp2 has a similar method |
868 | 889 | // but this is faster and does not fail if the stream is not found. |
869 | 890 | BaseObjectPtr<Http2Stream> Http2Session::FindStream(int32_t id) { |
@@ -958,11 +979,13 @@ void Http2Session::ConsumeHTTP2Data() { |
958 | 979 | nghttp2_session_want_read(session_.get())); |
959 | 980 | set_receive_paused(false); |
960 | 981 | custom_recv_error_code_ = nullptr; |
| 982 | + set_receiving(); |
961 | 983 | ssize_t ret = |
962 | 984 | nghttp2_session_mem_recv(session_.get(), |
963 | 985 | reinterpret_cast<uint8_t*>(stream_buf_.base) + |
964 | 986 | stream_buf_offset_, |
965 | 987 | read_len); |
| 988 | + set_receiving(false); |
966 | 989 | CHECK_NE(ret, NGHTTP2_ERR_NOMEM); |
967 | 990 | CHECK_IMPLIES(custom_recv_error_code_ != nullptr, ret < 0); |
968 | 991 |
|
@@ -976,6 +999,10 @@ void Http2Session::ConsumeHTTP2Data() { |
976 | 999 | // Even if all bytes were received, a paused stream may delay the |
977 | 1000 | // nghttp2_on_frame_recv_callback which may have an END_STREAM flag. |
978 | 1001 | 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(); |
979 | 1006 | goto done; |
980 | 1007 | } |
981 | 1008 |
|
@@ -986,12 +1013,23 @@ void Http2Session::ConsumeHTTP2Data() { |
986 | 1013 | stream_buf_allocation_.reset(); |
987 | 1014 | stream_buf_ = uv_buf_init(nullptr, 0); |
988 | 1015 |
|
| 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 | + |
989 | 1028 | // Send any data that was queued up while processing the received data. |
990 | 1029 | if (ret >= 0 && !is_destroyed()) { |
991 | 1030 | SendPendingData(); |
992 | 1031 | } |
993 | 1032 |
|
994 | | -done: |
995 | 1033 | if (ret < 0) [[unlikely]] { |
996 | 1034 | Isolate* isolate = env()->isolate(); |
997 | 1035 | Debug(this, |
@@ -1405,6 +1443,9 @@ int Http2Session::OnDataChunkReceived(nghttp2_session* handle, |
1405 | 1443 | len -= avail; |
1406 | 1444 | stream->EmitRead(avail, buf); |
1407 | 1445 |
|
| 1446 | + // JS may have destroyed the stream from inside onread; stop delivering. |
| 1447 | + if (stream->is_destroyed()) break; |
| 1448 | + |
1408 | 1449 | // If the stream owner (e.g. the JS Http2Stream) wants more data, just |
1409 | 1450 | // tell nghttp2 that all data has been consumed. Otherwise, defer until |
1410 | 1451 | // more data is being requested. |
@@ -1962,6 +2003,7 @@ uint8_t Http2Session::SendPendingData() { |
1962 | 2003 | // SendPendingData should not be called recursively. |
1963 | 2004 | if (is_sending()) |
1964 | 2005 | return 1; |
| 2006 | + |
1965 | 2007 | // This is cleared by ClearOutgoing(). |
1966 | 2008 | set_sending(); |
1967 | 2009 |
|
@@ -2372,10 +2414,48 @@ void Http2Stream::Destroy() { |
2372 | 2414 | // Do nothing if this stream instance is already destroyed |
2373 | 2415 | if (is_destroyed()) |
2374 | 2416 | return; |
2375 | | - if (session_->has_pending_rststream(id_)) |
2376 | | - FlushRstStream(); |
| 2417 | + |
| 2418 | + // Session may already be gone if destroy was deferred across a session |
| 2419 | + // teardown. |
| 2420 | + if (!session_) { |
| 2421 | + set_destroyed(); |
| 2422 | + Detach(); |
| 2423 | + return; |
| 2424 | + } |
| 2425 | + |
| 2426 | + // Mark destroyed immediately so OnDataChunkReceived stops EmitRead into an |
| 2427 | + // already-destroyed JS stream (which would treat the byte count as errno). |
2377 | 2428 | set_destroyed(); |
2378 | 2429 |
|
| 2430 | + // While mem_recv is active, do not FlushRstStream or RemoveStream yet: |
| 2431 | + // - FlushRstStream would close the nghttp2 stream before queued response |
| 2432 | + // DATA can be mem_send'd after receive returns. |
| 2433 | + // - RemoveStream would make OnSendData/Provider::OnRead fail to FindStream. |
| 2434 | + // Pending RSTs stay in pending_rst_streams_ and are flushed from |
| 2435 | + // ClearOutgoing after the post-receive SendPendingData. |
| 2436 | + if (session_->is_receiving()) { |
| 2437 | + BaseObjectPtr<Http2Stream> strong_ref{this}; |
| 2438 | + env()->SetImmediate( |
| 2439 | + [this, strong_ref](Environment*) { CompleteDestroyCleanup(); }); |
| 2440 | + return; |
| 2441 | + } |
| 2442 | + |
| 2443 | + if (session_->has_pending_rststream(id_)) FlushRstStream(); |
| 2444 | + |
| 2445 | + CompleteDestroyCleanup(); |
| 2446 | +} |
| 2447 | + |
| 2448 | +void Http2Stream::CompleteDestroyCleanup() { |
| 2449 | + if (!session_) { |
| 2450 | + Detach(); |
| 2451 | + return; |
| 2452 | + } |
| 2453 | + |
| 2454 | + // Destroy() always set_destroyed() before scheduling or calling this. |
| 2455 | + CHECK(is_destroyed()); |
| 2456 | + |
| 2457 | + if (session_->has_pending_rststream(id_)) FlushRstStream(); |
| 2458 | + |
2379 | 2459 | Debug(this, "destroying stream"); |
2380 | 2460 |
|
2381 | 2461 | // Wait until the start of the next loop to delete because there |
@@ -2412,7 +2492,6 @@ void Http2Stream::Destroy() { |
2412 | 2492 | EmitStatistics(); |
2413 | 2493 | } |
2414 | 2494 |
|
2415 | | - |
2416 | 2495 | // Initiates a response on the Http2Stream using data provided via the |
2417 | 2496 | // StreamBase Streams API. |
2418 | 2497 | int Http2Stream::SubmitResponse(const Http2Headers& headers, int options) { |
@@ -2521,6 +2600,18 @@ void Http2Stream::SubmitRstStream(const uint32_t code) { |
2521 | 2600 | return code == NGHTTP2_CANCEL; |
2522 | 2601 | }; |
2523 | 2602 |
|
| 2603 | + // Do not call `nghttp2_session_mem_send()` while nghttp2 is processing |
| 2604 | + // incoming data. Sending may close the stream and free nghttp2 state |
| 2605 | + // that is still in use by `nghttp2_session_mem_recv()`. |
| 2606 | + if (session_->is_receiving() && available_outbound_length_ == 0) { |
| 2607 | + if (is_stream_cancel(code)) { |
| 2608 | + session_->AddPendingRstStream(id_); |
| 2609 | + return; |
| 2610 | + } |
| 2611 | + FlushRstStream(); |
| 2612 | + return; |
| 2613 | + } |
| 2614 | + |
2524 | 2615 | // If RST_STREAM frame is received with error code NGHTTP2_CANCEL, |
2525 | 2616 | // add it to the pending list and don't force purge the data. It is |
2526 | 2617 | // to avoids the double free error due to unwanted behavior of nghttp2. |
@@ -2556,8 +2647,8 @@ void Http2Stream::SubmitRstStream(const uint32_t code) { |
2556 | 2647 | } |
2557 | 2648 |
|
2558 | 2649 | void Http2Stream::FlushRstStream() { |
2559 | | - if (is_destroyed()) |
2560 | | - return; |
| 2650 | + if (!session_) return; |
| 2651 | + session_->RemovePendingRstStream(id_); |
2561 | 2652 | Http2Scope h2scope(this); |
2562 | 2653 | CHECK_EQ(nghttp2_submit_rst_stream( |
2563 | 2654 | session_->session(), |
|