Abort async QPACK header decompression when stream is closed. This is to make sure QuicSpdyStream::OnHeadersDecoded() is not called by QpackDecodedHeadersAccumulator after the stream is closed. This is important, because OnHeadersDecoded() calls OnDataAvailable(), which can cause issues in higher layers. Protected by FLAGS_quic_reloadable_flag_quic_abort_qpack_on_stream_close. PiperOrigin-RevId: 332106028 Change-Id: I0d2e2069c63931cf8682d1f9f79d34644561d266
diff --git a/quic/core/http/quic_spdy_stream.cc b/quic/core/http/quic_spdy_stream.cc index 67c24e4..1c5f964 100644 --- a/quic/core/http/quic_spdy_stream.cc +++ b/quic/core/http/quic_spdy_stream.cc
@@ -801,6 +801,11 @@ void QuicSpdyStream::OnClose() { QuicStream::OnClose(); + if (GetQuicReloadableFlag(quic_abort_qpack_on_stream_close)) { + QUIC_RELOADABLE_FLAG_COUNT(quic_abort_qpack_on_stream_close); + qpack_decoded_headers_accumulator_.reset(); + } + if (visitor_) { Visitor* visitor = visitor_; // Calling Visitor::OnClose() may result the destruction of the visitor,
diff --git a/quic/core/http/quic_spdy_stream_test.cc b/quic/core/http/quic_spdy_stream_test.cc index cc0e3ec..85e6467 100644 --- a/quic/core/http/quic_spdy_stream_test.cc +++ b/quic/core/http/quic_spdy_stream_test.cc
@@ -2416,6 +2416,64 @@ session_->qpack_decoder()->OnInsertWithoutNameReference("trailing", "foobar"); } +// Regression test for b/132603592: QPACK decoding unblocked after stream is +// closed. +TEST_P(QuicSpdyStreamTest, HeaderDecodingUnblockedAfterStreamClosed) { + if (!UsesHttp3()) { + return; + } + + Initialize(kShouldProcessData); + testing::InSequence s; + session_->qpack_decoder()->OnSetDynamicTableCapacity(1024); + StrictMock<MockHttp3DebugVisitor> debug_visitor; + session_->set_debug_visitor(&debug_visitor); + + // HEADERS frame referencing first dynamic table entry. + std::string encoded_headers = quiche::QuicheTextUtils::HexDecode("020080"); + std::string headers = HeadersFrame(encoded_headers); + EXPECT_CALL(debug_visitor, + OnHeadersFrameReceived(stream_->id(), encoded_headers.length())); + stream_->OnStreamFrame(QuicStreamFrame(stream_->id(), false, 0, headers)); + + // Decoding is blocked because dynamic table entry has not been received yet. + EXPECT_FALSE(stream_->headers_decompressed()); + + // Decoder stream type and stream cancellation instruction. + auto decoder_send_stream = + QuicSpdySessionPeer::GetQpackDecoderSendStream(session_.get()); + EXPECT_CALL(*session_, + WritevData(decoder_send_stream->id(), /* write_length = */ 1, + /* offset = */ 0, _, _, _)); + EXPECT_CALL(*session_, + WritevData(decoder_send_stream->id(), /* write_length = */ 1, + /* offset = */ 1, _, _, _)); + + // Reset stream. + EXPECT_CALL(*session_, + SendRstStream(stream_->id(), QUIC_STREAM_CANCELLED, _, _)); + stream_->Reset(QUIC_STREAM_CANCELLED); + + if (!GetQuicReloadableFlag(quic_abort_qpack_on_stream_close)) { + // Header acknowledgement. + EXPECT_CALL(*session_, + WritevData(decoder_send_stream->id(), /* write_length = */ 1, + /* offset = */ 2, _, _, _)); + EXPECT_CALL(debug_visitor, OnHeadersDecoded(stream_->id(), _)); + } + + // Deliver dynamic table entry to decoder. + session_->qpack_decoder()->OnInsertWithoutNameReference("foo", "bar"); + + if (GetQuicReloadableFlag(quic_abort_qpack_on_stream_close)) { + EXPECT_FALSE(stream_->headers_decompressed()); + } else { + // Verify headers. + EXPECT_TRUE(stream_->headers_decompressed()); + EXPECT_THAT(stream_->header_list(), ElementsAre(Pair("foo", "bar"))); + } +} + class QuicSpdyStreamIncrementalConsumptionTest : public QuicSpdyStreamTest { protected: QuicSpdyStreamIncrementalConsumptionTest() : offset_(0), consumed_bytes_(0) {}