Patch in 248417588: renjietang Send HEADERS on the request stream
diff --git a/quic/core/http/end_to_end_test.cc b/quic/core/http/end_to_end_test.cc index c72622c..59159a5 100644 --- a/quic/core/http/end_to_end_test.cc +++ b/quic/core/http/end_to_end_test.cc
@@ -11,6 +11,21 @@ #include <utility> #include <vector> +<<<<<<< +||||||| +#include "net/third_party/quic/core/crypto/null_encrypter.h" +#include "net/third_party/quic/core/http/quic_spdy_client_stream.h" +#include "net/third_party/quic/core/quic_epoll_connection_helper.h" +#include "net/third_party/quic/core/quic_error_codes.h" +#include "net/third_party/quic/core/quic_framer.h" +======= +#include "net/third_party/quic/core/crypto/null_encrypter.h" +#include "net/third_party/quic/core/http/quic_spdy_client_stream.h" +#include "net/third_party/quic/core/qpack/qpack_encoder_test_utils.h" +#include "net/third_party/quic/core/quic_epoll_connection_helper.h" +#include "net/third_party/quic/core/quic_error_codes.h" +#include "net/third_party/quic/core/quic_framer.h" +>>>>>>> #include "net/third_party/quiche/src/quic/core/crypto/null_encrypter.h" #include "net/third_party/quiche/src/quic/core/http/quic_spdy_client_stream.h" #include "net/third_party/quiche/src/quic/core/quic_epoll_connection_helper.h" @@ -1368,8 +1383,17 @@ headers["key3"] = std::string(15 * 1024, 'a'); client_->SendCustomSynchronousRequest(headers, body); - EXPECT_EQ(QUIC_HEADERS_TOO_LARGE, client_->stream_error()); - EXPECT_EQ(QUIC_NO_ERROR, client_->connection_error()); + + if (VersionUsesQpack(client_->client() + ->client_session() + ->connection() + ->transport_version())) { + EXPECT_EQ(QUIC_HEADERS_STREAM_DATA_DECOMPRESS_FAILURE, + client_->connection_error()); + } else { + EXPECT_EQ(QUIC_HEADERS_TOO_LARGE, client_->stream_error()); + EXPECT_EQ(QUIC_NO_ERROR, client_->connection_error()); + } } TEST_P(EndToEndTest, EarlyResponseWithQuicStreamNoError) { @@ -2007,10 +2031,9 @@ client_->client()->client_session()); // In v47 and later, the crypto handshake (sent in CRYPTO frames) is not // subject to flow control. - if (!QuicVersionUsesCryptoFrames(client_->client() - ->client_session() - ->connection() - ->transport_version())) { + const QuicTransportVersion transport_version = + client_->client()->client_session()->connection()->transport_version(); + if (!QuicVersionUsesCryptoFrames(transport_version)) { EXPECT_LT(QuicFlowControllerPeer::SendWindowSize( crypto_stream->flow_controller()), kStreamIFCW); @@ -2023,6 +2046,11 @@ // has not been affected. EXPECT_EQ(kFooResponseBody, client_->SendSynchronousRequest("/foo")); + // No headers stream in IETF QUIC. + if (VersionUsesQpack(transport_version)) { + return; + } + QuicHeadersStream* headers_stream = QuicSpdySessionPeer::GetHeadersStream( client_->client()->client_session()); EXPECT_LT( @@ -2187,6 +2215,29 @@ client_->SendMessage(headers, "", /*fin=*/false); + // Size of headers on the request stream. Zero if headers are sent on the + // header stream. + size_t header_size = 0; + if (VersionUsesQpack(client_->client() + ->client_session() + ->connection() + ->transport_version())) { + // Determine size of compressed headers. + NoopDecoderStreamErrorDelegate decoder_stream_error_delegate; + NoopEncoderStreamSenderDelegate encoder_stream_sender_delegate; + QpackEncoder qpack_encoder(&decoder_stream_error_delegate, + &encoder_stream_sender_delegate); + auto progressive_encoder = + qpack_encoder.EncodeHeaderList(/* stream_id = */ 0, &headers); + std::string encoded_headers; + while (progressive_encoder->HasNext()) { + progressive_encoder->Next( + /* max_encoded_bytes = */ std::numeric_limits<size_t>::max(), + &encoded_headers); + } + header_size = encoded_headers.size(); + } + // Test the AckNotifier's ability to track multiple packets by making the // request body exceed the size of a single packet. std::string request_string = "a request body bigger than one packet" + @@ -2194,7 +2245,7 @@ // The TestAckListener will cause a failure if not notified. QuicReferenceCountedPointer<TestAckListener> ack_listener( - new TestAckListener(request_string.length())); + new TestAckListener(header_size + request_string.length())); // Send the request, and register the delegate for ACKs. client_->SendData(request_string, true, ack_listener);
diff --git a/quic/core/http/quic_headers_stream_test.cc b/quic/core/http/quic_headers_stream_test.cc index 2f669df..a41235f 100644 --- a/quic/core/http/quic_headers_stream_test.cc +++ b/quic/core/http/quic_headers_stream_test.cc
@@ -370,6 +370,10 @@ } TEST_P(QuicHeadersStreamTest, WriteHeaders) { + if (VersionUsesQpack(transport_version())) { + return; + } + for (QuicStreamId stream_id = client_id_1_; stream_id < client_id_3_; stream_id += next_stream_id_) { for (bool fin : {false, true}) { @@ -421,6 +425,10 @@ } TEST_P(QuicHeadersStreamTest, ProcessRawData) { + if (VersionUsesQpack(transport_version())) { + return; + } + for (QuicStreamId stream_id = client_id_1_; stream_id < client_id_3_; stream_id += next_stream_id_) { for (bool fin : {false, true}) { @@ -536,6 +544,10 @@ } TEST_P(QuicHeadersStreamTest, ProcessLargeRawData) { + if (VersionUsesQpack(transport_version())) { + return; + } + QuicSpdySessionPeer::SetMaxUncompressedHeaderBytes(&session_, 256 * 1024); // We want to create a frame that is more than the SPDY Framer's max control // frame size, which is 16K, but less than the HPACK decoders max decode @@ -710,6 +722,10 @@ } TEST_P(QuicHeadersStreamTest, HpackDecoderDebugVisitor) { + if (VersionUsesQpack(transport_version())) { + return; + } + auto hpack_decoder_visitor = QuicMakeUnique<StrictMock<MockQuicHpackDebugVisitor>>(); { @@ -762,6 +778,10 @@ } TEST_P(QuicHeadersStreamTest, HpackEncoderDebugVisitor) { + if (VersionUsesQpack(transport_version())) { + return; + } + auto hpack_encoder_visitor = QuicMakeUnique<StrictMock<MockQuicHpackDebugVisitor>>(); if (perspective() == Perspective::IS_SERVER) {
diff --git a/quic/core/http/quic_spdy_client_stream_test.cc b/quic/core/http/quic_spdy_client_stream_test.cc index 007bba0..5f5d5c1 100644 --- a/quic/core/http/quic_spdy_client_stream_test.cc +++ b/quic/core/http/quic_spdy_client_stream_test.cc
@@ -193,9 +193,14 @@ EXPECT_NE(QUIC_STREAM_NO_ERROR, stream_->stream_error()); } +// Test that receiving trailing headers (on the headers stream), containing a +// final offset, results in the stream being closed at that byte offset. TEST_P(QuicSpdyClientStreamTest, ReceivingTrailers) { - // Test that receiving trailing headers, containing a final offset, results in - // the stream being closed at that byte offset. + // There is no kFinalOffsetHeaderKey if trailers are sent on the + // request/response stream. + if (VersionUsesQpack(connection_->transport_version())) { + return; + } // Send headers as usual. auto headers = AsHeaderList(headers_);
diff --git a/quic/core/http/quic_spdy_session.cc b/quic/core/http/quic_spdy_session.cc index bdaa433..93d10e1 100644 --- a/quic/core/http/quic_spdy_session.cc +++ b/quic/core/http/quic_spdy_session.cc
@@ -204,6 +204,12 @@ return; } + if (VersionUsesQpack(session_->connection()->transport_version())) { + CloseConnection("HEADERS frame not allowed on headers stream.", + QUIC_INVALID_HEADERS_STREAM_DATA); + return; + } + // TODO(mpw): avoid down-conversion and plumb SpdyStreamPrecedence through // QuicHeadersStream. SpdyPriority priority = @@ -399,7 +405,6 @@ DCHECK(VersionUsesQpack(connection()->transport_version())); // TODO(112770235): Send decoder stream data on decoder stream. - QUIC_NOTREACHED(); } void QuicSpdySession::OnStreamHeadersPriority(QuicStreamId stream_id, @@ -472,6 +477,8 @@ bool fin, SpdyPriority priority, QuicReferenceCountedPointer<QuicAckListenerInterface> ack_listener) { + DCHECK(!VersionUsesQpack(connection()->transport_version())); + return WriteHeadersOnHeadersStreamImpl( id, std::move(headers), fin, /* parent_stream_id = */ 0, Spdy3PriorityToHttp2Weight(priority), @@ -568,6 +575,8 @@ int weight, bool exclusive, QuicReferenceCountedPointer<QuicAckListenerInterface> ack_listener) { + DCHECK(!VersionUsesQpack(connection()->transport_version())); + SpdyHeadersIR headers_frame(id, std::move(headers)); headers_frame.set_fin(fin); if (perspective() == Perspective::IS_CLIENT) {
diff --git a/quic/core/http/quic_spdy_session_test.cc b/quic/core/http/quic_spdy_session_test.cc index 0e2fcf8..1a33fd1 100644 --- a/quic/core/http/quic_spdy_session_test.cc +++ b/quic/core/http/quic_spdy_session_test.cc
@@ -1221,6 +1221,13 @@ if (QuicVersionUsesCryptoFrames(connection_->transport_version())) { return; } + + // This test depends on the headers stream, which does not exist when QPACK is + // used. + if (VersionUsesQpack(transport_version())) { + return; + } + // Test that if the header stream is flow control blocked, then if the SHLO // contains a larger send window offset, the stream becomes unblocked. session_.set_writev_consumes_all_data(true);
diff --git a/quic/core/http/quic_spdy_stream.cc b/quic/core/http/quic_spdy_stream.cc index d04efc5..2f161ae 100644 --- a/quic/core/http/quic_spdy_stream.cc +++ b/quic/core/http/quic_spdy_stream.cc
@@ -4,11 +4,22 @@ #include "net/third_party/quiche/src/quic/core/http/quic_spdy_stream.h" +#include <limits> #include <string> #include <utility> #include "net/third_party/quiche/src/quic/core/http/quic_spdy_session.h" #include "net/third_party/quiche/src/quic/core/http/spdy_utils.h" +#include "net/third_party/quiche/src/quic/core/qpack/qpack_decoded_headers_accumulator.h" +#include "net/third_party/quiche/src/quic/core/qpack/qpack_decoder.h" +#include "net/third_party/quiche/src/quic/core/qpack/qpack_encoder.h" +#include "net/third_party/quiche/src/quic/core/quic_utils.h" +#include "net/third_party/quiche/src/quic/core/quic_versions.h" +#include "net/third_party/quiche/src/quic/core/quic_write_blocked_list.h" +#include "net/third_party/quiche/src/quic/platform/api/quic_bug_tracker.h" +#include "net/third_party/quiche/src/quic/platform/api/quic_flag_utils.h" +#include "net/third_party/quiche/src/quic/core/http/quic_spdy_session.h" +#include "net/third_party/quiche/src/quic/core/http/spdy_utils.h" #include "net/third_party/quiche/src/quic/core/quic_utils.h" #include "net/third_party/quiche/src/quic/core/quic_write_blocked_list.h" #include "net/third_party/quiche/src/quic/platform/api/quic_bug_tracker.h" @@ -139,8 +150,11 @@ StreamType type) : QuicStream(id, spdy_session, /*is_static=*/false, type), spdy_session_(spdy_session), + on_body_available_called_because_sequencer_is_closed_(false), visitor_(nullptr), headers_decompressed_(false), + headers_length_(0, 0), + trailers_length_(0, 0), trailers_decompressed_(false), trailers_consumed_(false), http_decoder_visitor_(new HttpDecoderVisitor(this)), @@ -148,9 +162,11 @@ ack_listener_(nullptr) { DCHECK(!QuicUtils::IsCryptoStreamId( spdy_session->connection()->transport_version(), id)); - // Don't receive any callbacks from the sequencer until headers - // are complete. - sequencer()->SetBlockedUntilFlush(); + // If headers are sent on the headers stream, then do not receive any + // callbacks from the sequencer until headers are complete. + if (!VersionUsesQpack(spdy_session_->connection()->transport_version())) { + sequencer()->SetBlockedUntilFlush(); + } if (VersionHasDataFrameHeader( spdy_session_->connection()->transport_version())) { @@ -164,8 +180,11 @@ StreamType type) : QuicStream(std::move(pending), type, /*is_static=*/false), spdy_session_(spdy_session), + on_body_available_called_because_sequencer_is_closed_(false), visitor_(nullptr), headers_decompressed_(false), + headers_length_(0, 0), + trailers_length_(0, 0), trailers_decompressed_(false), trailers_consumed_(false), http_decoder_visitor_(new HttpDecoderVisitor(this)), @@ -173,9 +192,11 @@ ack_listener_(nullptr) { DCHECK(!QuicUtils::IsCryptoStreamId( spdy_session->connection()->transport_version(), id())); - // Don't receive any callbacks from the sequencer until headers - // are complete. - sequencer()->SetBlockedUntilFlush(); + // If headers are sent on the headers stream, then do not receive any + // callbacks from the sequencer until headers are complete. + if (!VersionUsesQpack(spdy_session_->connection()->transport_version())) { + sequencer()->SetBlockedUntilFlush(); + } if (VersionHasDataFrameHeader( spdy_session_->connection()->transport_version())) { @@ -192,7 +213,11 @@ QuicReferenceCountedPointer<QuicAckListenerInterface> ack_listener) { size_t bytes_written = WriteHeadersImpl(std::move(header_block), fin, std::move(ack_listener)); - if (fin) { + if (!VersionUsesQpack(spdy_session_->connection()->transport_version()) && + fin) { + // If HEADERS are sent on the headers stream, then |fin_sent_| needs to be + // set and write side needs to be closed without actually sending a FIN on + // this stream. // TODO(rch): Add test to ensure fin_sent_ is set whenever a fin is sent. set_fin_sent(true); CloseWriteSide(); @@ -217,13 +242,15 @@ unacked_frame_headers_offsets_.Add( send_buffer().stream_offset(), send_buffer().stream_offset() + header_length); - QUIC_DLOG(INFO) << "Stream " << id() << " is writing header of length " + QUIC_DLOG(INFO) << "Stream " << id() + << " is writing DATA frame header of length " << header_length; WriteOrBufferData(QuicStringPiece(buffer.get(), header_length), false, nullptr); // Write body. - QUIC_DLOG(INFO) << "Stream " << id() << " is writing body of length " + QUIC_DLOG(INFO) << "Stream " << id() + << " is writing DATA frame payload of length " << data.length(); WriteOrBufferData(data, fin, nullptr); } @@ -236,26 +263,33 @@ return 0; } - // The header block must contain the final offset for this stream, as the - // trailers may be processed out of order at the peer. - QUIC_DLOG(INFO) << "Inserting trailer: (" << kFinalOffsetHeaderKey << ", " - << stream_bytes_written() + BufferedDataBytes() << ")"; - trailer_block.insert( - std::make_pair(kFinalOffsetHeaderKey, - QuicTextUtils::Uint64ToString(stream_bytes_written() + - BufferedDataBytes()))); + if (!VersionUsesQpack(spdy_session_->connection()->transport_version())) { + // The header block must contain the final offset for this stream, as the + // trailers may be processed out of order at the peer. + const QuicStreamOffset final_offset = + stream_bytes_written() + BufferedDataBytes(); + QUIC_DLOG(INFO) << "Inserting trailer: (" << kFinalOffsetHeaderKey << ", " + << final_offset << ")"; + trailer_block.insert(std::make_pair( + kFinalOffsetHeaderKey, QuicTextUtils::Uint64ToString(final_offset))); + } // Write the trailing headers with a FIN, and close stream for writing: // trailers are the last thing to be sent on a stream. const bool kFin = true; size_t bytes_written = WriteHeadersImpl(std::move(trailer_block), kFin, std::move(ack_listener)); - set_fin_sent(kFin); - // Trailers are the last thing to be sent on a stream, but if there is still - // queued data then CloseWriteSide() will cause it never to be sent. - if (BufferedDataBytes() == 0) { - CloseWriteSide(); + // If trailers are sent on the headers stream, then |fin_sent_| needs to be + // set without actually sending a FIN on this stream. + if (!VersionUsesQpack(spdy_session_->connection()->transport_version())) { + set_fin_sent(kFin); + + // Also, write side of this stream needs to be closed. However, only do + // this if there is no more buffered data, otherwise it will never be sent. + if (BufferedDataBytes() == 0) { + CloseWriteSide(); + } } return bytes_written; @@ -298,12 +332,14 @@ unacked_frame_headers_offsets_.Add( send_buffer().stream_offset(), send_buffer().stream_offset() + header_length); - QUIC_DLOG(INFO) << "Stream " << id() << " is writing header of length " + QUIC_DLOG(INFO) << "Stream " << id() + << " is writing DATA frame header of length " << header_length; WriteMemSlices(storage.ToSpan(), false); // Write body. - QUIC_DLOG(INFO) << "Stream " << id() << " is writing body of length " + QUIC_DLOG(INFO) << "Stream " << id() + << " is writing DATA frame payload of length " << slices.total_length(); return WriteMemSlices(slices, fin); } @@ -352,6 +388,15 @@ } void QuicSpdyStream::MarkTrailersConsumed() { + /*if (VersionUsesQpack(spdy_session_->connection()->transport_version()) && + !reading_stopped()) { + const QuicByteCount trailers_total_length = + trailers_length_.header_length + trailers_length_.payload_length; + if (trailers_total_length > 0) { + sequencer()->MarkConsumed(trailers_total_length); + } + }*/ + trailers_consumed_ = true; } @@ -365,8 +410,35 @@ void QuicSpdyStream::ConsumeHeaderList() { header_list_.Clear(); - if (FinishedReadingHeaders()) { - sequencer()->SetUnblocked(); + + if (!VersionUsesQpack(spdy_session_->connection()->transport_version())) { + if (FinishedReadingHeaders()) { + sequencer()->SetUnblocked(); + } + return; + } + + /*if (!reading_stopped()) { + const QuicByteCount headers_total_length = + headers_length_.header_length + headers_length_.payload_length; + if (headers_total_length > 0) { + sequencer()->MarkConsumed(headers_total_length); + } + }*/ + + if (!FinishedReadingHeaders()) { + return; + } + + if (body_buffer_.HasBytesToRead()) { + OnBodyAvailable(); + return; + } + + if (sequencer()->IsClosed() && + !on_body_available_called_because_sequencer_is_closed_) { + on_body_available_called_because_sequencer_is_closed_ = true; + OnBodyAvailable(); } } @@ -398,7 +470,15 @@ } void QuicSpdyStream::OnHeadersTooLarge() { - Reset(QUIC_HEADERS_TOO_LARGE); + if (VersionUsesQpack(spdy_session_->connection()->transport_version())) { + // TODO(124216424): Use HTTP_EXCESSIVE_LOAD error code. + std::string error_message = + QuicStrCat("Too large headers received on stream ", id()); + CloseConnectionWithDetails(QUIC_HEADERS_STREAM_DATA_DECOMPRESS_FAILURE, + error_message); + } else { + Reset(QUIC_HEADERS_TOO_LARGE); + } } void QuicSpdyStream::OnInitialHeadersComplete( @@ -407,8 +487,20 @@ const QuicHeaderList& header_list) { headers_decompressed_ = true; header_list_ = header_list; + + if (VersionUsesQpack(spdy_session_->connection()->transport_version())) { + if (fin) { + OnStreamFrame( + QuicStreamFrame(id(), /* fin = */ true, + flow_controller()->highest_received_byte_offset(), + QuicStringPiece())); + } + return; + } + if (fin) { - OnStreamFrame(QuicStreamFrame(id(), fin, 0, QuicStringPiece())); + OnStreamFrame( + QuicStreamFrame(id(), fin, /* offset = */ 0, QuicStringPiece())); } if (FinishedReadingHeaders()) { sequencer()->SetUnblocked(); @@ -431,15 +523,18 @@ size_t /*frame_len*/, const QuicHeaderList& header_list) { DCHECK(!trailers_decompressed_); - if (fin_received()) { - QUIC_DLOG(ERROR) << "Received Trailers after FIN, on stream: " << id(); + if (!VersionUsesQpack(spdy_session_->connection()->transport_version()) && + fin_received()) { + QUIC_DLOG(INFO) << "Received Trailers after FIN, on stream: " << id(); session()->connection()->CloseConnection( QUIC_INVALID_HEADERS_STREAM_DATA, "Trailers after fin", ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET); return; } - if (!fin) { - QUIC_DLOG(ERROR) << "Trailers must have FIN set, on stream: " << id(); + + if (!VersionUsesQpack(spdy_session_->connection()->transport_version()) && + !fin) { + QUIC_DLOG(INFO) << "Trailers must have FIN set, on stream: " << id(); session()->connection()->CloseConnection( QUIC_INVALID_HEADERS_STREAM_DATA, "Fin missing from trailers", ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET); @@ -447,8 +542,9 @@ } size_t final_byte_offset = 0; - if (!SpdyUtils::CopyAndValidateTrailers(header_list, - /* expect_final_byte_offset = */ true, + const bool expect_final_byte_offset = + !VersionUsesQpack(spdy_session_->connection()->transport_version()); + if (!SpdyUtils::CopyAndValidateTrailers(header_list, expect_final_byte_offset, &final_byte_offset, &received_trailers_)) { QUIC_DLOG(ERROR) << "Trailers for stream " << id() << " are malformed."; @@ -458,16 +554,12 @@ return; } trailers_decompressed_ = true; + const QuicStreamOffset offset = + VersionUsesQpack(spdy_session_->connection()->transport_version()) + ? flow_controller()->highest_received_byte_offset() + : final_byte_offset; OnStreamFrame( - QuicStreamFrame(id(), fin, final_byte_offset, QuicStringPiece())); -} - -size_t QuicSpdyStream::WriteHeadersImpl( - spdy::SpdyHeaderBlock header_block, - bool fin, - QuicReferenceCountedPointer<QuicAckListenerInterface> ack_listener) { - return spdy_session_->WriteHeadersOnHeadersStream( - id(), std::move(header_block), fin, priority(), std::move(ack_listener)); + QuicStreamFrame(id(), /* fin = */ true, offset, QuicStringPiece())); } void QuicSpdyStream::OnPriorityFrame(SpdyPriority priority) { @@ -488,7 +580,10 @@ } void QuicSpdyStream::OnDataAvailable() { - DCHECK(FinishedReadingHeaders()); + if (!VersionUsesQpack(spdy_session_->connection()->transport_version())) { + // Sequencer must be blocked until headers are consumed. + DCHECK(FinishedReadingHeaders()); + } if (!VersionHasDataFrameHeader( session()->connection()->transport_version())) { @@ -502,14 +597,20 @@ iov.iov_len); } + // Do not call OnBodyAvailable() until headers are consumed. + if (!FinishedReadingHeaders()) { + return; + } + if (body_buffer_.HasBytesToRead()) { OnBodyAvailable(); return; } - if (sequencer()->IsClosed()) { + if (sequencer()->IsClosed() && + !on_body_available_called_because_sequencer_is_closed_) { + on_body_available_called_because_sequencer_is_closed_ = true; OnBodyAvailable(); - return; } } @@ -645,14 +746,98 @@ void QuicSpdyStream::OnHeadersFrameStart(Http3FrameLengths frame_length) { DCHECK(VersionUsesQpack(spdy_session_->connection()->transport_version())); + DCHECK(!qpack_decoded_headers_accumulator_); + + if (headers_decompressed_) { + trailers_length_ = frame_length; + } else { + headers_length_ = frame_length; + } + + qpack_decoded_headers_accumulator_ = + QuicMakeUnique<QpackDecodedHeadersAccumulator>( + id(), spdy_session_->qpack_decoder(), + spdy_session_->max_inbound_header_list_size()); + sequencer()->MarkConsumed(frame_length.header_length); } void QuicSpdyStream::OnHeadersFramePayload(QuicStringPiece payload) { DCHECK(VersionUsesQpack(spdy_session_->connection()->transport_version())); + + if (!qpack_decoded_headers_accumulator_->Decode(payload)) { + // TODO(124216424): Use HTTP_QPACK_DECOMPRESSION_FAILED error code. + std::string error_message = + QuicStrCat("Error decompressing header block on stream ", id(), ": ", + qpack_decoded_headers_accumulator_->error_message()); + CloseConnectionWithDetails(QUIC_DECOMPRESSION_FAILURE, error_message); + return; + } + sequencer()->MarkConsumed(payload.size()); } void QuicSpdyStream::OnHeadersFrameEnd() { DCHECK(VersionUsesQpack(spdy_session_->connection()->transport_version())); + + if (!qpack_decoded_headers_accumulator_->EndHeaderBlock()) { + // TODO(124216424): Use HTTP_QPACK_DECOMPRESSION_FAILED error code. + std::string error_message = + QuicStrCat("Error decompressing header block on stream ", id(), ": ", + qpack_decoded_headers_accumulator_->error_message()); + CloseConnectionWithDetails(QUIC_DECOMPRESSION_FAILURE, error_message); + return; + } + + const QuicByteCount frame_length = headers_decompressed_ + ? trailers_length_.payload_length + : headers_length_.payload_length; + OnStreamHeaderList(/* fin = */ false, frame_length, + qpack_decoded_headers_accumulator_->quic_header_list()); + + qpack_decoded_headers_accumulator_.reset(); +} + +size_t QuicSpdyStream::WriteHeadersImpl( + spdy::SpdyHeaderBlock header_block, + bool fin, + QuicReferenceCountedPointer<QuicAckListenerInterface> ack_listener) { + if (!VersionUsesQpack(spdy_session_->connection()->transport_version())) { + return spdy_session_->WriteHeadersOnHeadersStream( + id(), std::move(header_block), fin, priority(), + std::move(ack_listener)); + } + + // Encode header list. + auto progressive_encoder = spdy_session_->qpack_encoder()->EncodeHeaderList( + /* stream_id = */ id(), &header_block); + std::string encoded_headers; + while (progressive_encoder->HasNext()) { + progressive_encoder->Next( + /* max_encoded_bytes = */ std::numeric_limits<size_t>::max(), + &encoded_headers); + } + + // Write HEADERS frame. + std::unique_ptr<char[]> headers_frame_header; + const size_t headers_frame_header_length = + encoder_.SerializeHeadersFrameHeader(encoded_headers.size(), + &headers_frame_header); + unacked_frame_headers_offsets_.Add( + send_buffer().stream_offset(), + send_buffer().stream_offset() + headers_frame_header_length); + + QUIC_DLOG(INFO) << "Stream " << id() + << " is writing HEADERS frame header of length " + << headers_frame_header_length; + WriteOrBufferData( + QuicStringPiece(headers_frame_header.get(), headers_frame_header_length), + /* fin = */ false, /* ack_listener = */ nullptr); + + QUIC_DLOG(INFO) << "Stream " << id() + << " is writing HEADERS frame payload of length " + << encoded_headers.length(); + WriteOrBufferData(encoded_headers, fin, nullptr); + + return encoded_headers.size(); } #undef ENDPOINT // undef for jumbo builds
diff --git a/quic/core/http/quic_spdy_stream.h b/quic/core/http/quic_spdy_stream.h index f81a421..3c5a166 100644 --- a/quic/core/http/quic_spdy_stream.h +++ b/quic/core/http/quic_spdy_stream.h
@@ -34,6 +34,7 @@ class QuicStreamPeer; } // namespace test +class QpackDecodedHeadersAccumulator; class QuicSpdySession; // A QUIC stream that can send and receive HTTP2 (SPDY) headers. @@ -105,8 +106,8 @@ // Called in OnDataAvailable() after it finishes the decoding job. virtual void OnBodyAvailable() = 0; - // Writes the headers contained in |header_block| to the dedicated - // headers stream. + // Writes the headers contained in |header_block| on the dedicated headers + // stream or on this stream, depending on VersionUsesQpack(). virtual size_t WriteHeaders( spdy::SpdyHeaderBlock header_block, bool fin, @@ -115,8 +116,9 @@ // Sends |data| to the peer, or buffers if it can't be sent immediately. void WriteOrBufferBody(QuicStringPiece data, bool fin); - // Writes the trailers contained in |trailer_block| to the dedicated - // headers stream. Trailers will always have the FIN set. + // Writes the trailers contained in |trailer_block| on the dedicated headers + // stream or on this stream, depending on VersionUsesQpack(). Trailers will + // always have the FIN flag set. virtual size_t WriteTrailers( spdy::SpdyHeaderBlock trailer_block, QuicReferenceCountedPointer<QuicAckListenerInterface> ack_listener); @@ -247,12 +249,18 @@ QuicSpdySession* spdy_session_; + bool on_body_available_called_because_sequencer_is_closed_; + Visitor* visitor_; // True if the headers have been completely decompressed. bool headers_decompressed_; // Contains a copy of the decompressed header (name, value) pairs until they // are consumed via Readv. QuicHeaderList header_list_; + // Length of HEADERS frame, including frame header and payload. + Http3FrameLengths headers_length_; + // Length of TRAILERS frame, including frame header and payload. + Http3FrameLengths trailers_length_; // True if the trailers have been completely decompressed. bool trailers_decompressed_; @@ -265,6 +273,9 @@ HttpEncoder encoder_; // Http decoder for processing raw incoming stream frames. HttpDecoder decoder_; + // Headers accumulator for decoding HEADERS frame payload. + std::unique_ptr<QpackDecodedHeadersAccumulator> + qpack_decoded_headers_accumulator_; // Visitor of the HttpDecoder. std::unique_ptr<HttpDecoderVisitor> http_decoder_visitor_; // Buffer that contains decoded data of the stream.
diff --git a/quic/core/http/quic_spdy_stream_test.cc b/quic/core/http/quic_spdy_stream_test.cc index c45a899..eb9a0b3 100644 --- a/quic/core/http/quic_spdy_stream_test.cc +++ b/quic/core/http/quic_spdy_stream_test.cc
@@ -78,6 +78,11 @@ ack_listener) override { saved_headers_ = std::move(header_block); WriteHeadersMock(fin); + if (VersionUsesQpack(transport_version())) { + // In this case, call QuicSpdyStream::WriteHeadersImpl() that does the + // actual work of closing the stream. + QuicSpdyStream::WriteHeadersImpl(saved_headers_.Clone(), fin, nullptr); + } return 0; } @@ -212,10 +217,23 @@ QuicHeaderList headers; stream_->OnStreamHeadersPriority(kV3HighestPriority); - EXPECT_CALL(*session_, - SendRstStream(stream_->id(), QUIC_HEADERS_TOO_LARGE, 0)); + const bool version_uses_qpack = + VersionUsesQpack(connection_->transport_version()); + + if (version_uses_qpack) { + EXPECT_CALL(*connection_, + CloseConnection(QUIC_HEADERS_STREAM_DATA_DECOMPRESS_FAILURE, + "Too large headers received on stream 4", _)); + } else { + EXPECT_CALL(*session_, + SendRstStream(stream_->id(), QUIC_HEADERS_TOO_LARGE, 0)); + } + stream_->OnStreamHeaderList(false, 1 << 20, headers); - EXPECT_EQ(QUIC_HEADERS_TOO_LARGE, stream_->stream_error()); + + if (!version_uses_qpack) { + EXPECT_EQ(QUIC_HEADERS_TOO_LARGE, stream_->stream_error()); + } } TEST_P(QuicSpdyStreamTest, ProcessHeaderListWithFin) { @@ -919,7 +937,11 @@ trailers_block["key2"] = "value2"; trailers_block["key3"] = "value3"; SpdyHeaderBlock trailers_block_with_final_offset = trailers_block.Clone(); - trailers_block_with_final_offset[kFinalOffsetHeaderKey] = "0"; + if (!VersionUsesQpack(GetParam().transport_version)) { + // :final-offset pseudo-header is only added if trailers are sent + // on the headers stream. + trailers_block_with_final_offset[kFinalOffsetHeaderKey] = "0"; + } total_bytes = 0; QuicHeaderList trailers; for (const auto& p : trailers_block_with_final_offset) { @@ -943,6 +965,12 @@ // body, stream is closed at the right offset. Initialize(kShouldProcessData); + // kFinalOffsetHeaderKey is not used when HEADERS are sent on the + // request/response stream. + if (VersionUsesQpack(GetParam().transport_version)) { + return; + } + // Receive initial headers. QuicHeaderList headers = ProcessHeaders(false, headers_); stream_->ConsumeHeaderList(); @@ -988,6 +1016,12 @@ // Test that receiving trailers without a final offset field is an error. Initialize(kShouldProcessData); + // kFinalOffsetHeaderKey is not used when HEADERS are sent on the + // request/response stream. + if (VersionUsesQpack(GetParam().transport_version)) { + return; + } + // Receive initial headers. ProcessHeaders(false, headers_); stream_->ConsumeHeaderList(); @@ -1010,10 +1044,60 @@ trailers.uncompressed_header_bytes(), trailers); } +TEST_P(QuicSpdyStreamTest, ReceivingTrailersOnRequestStream) { + Initialize(kShouldProcessData); + + if (!VersionUsesQpack(GetParam().transport_version)) { + return; + } + + // Receive initial headers. + QuicHeaderList headers = ProcessHeaders(false, headers_); + stream_->ConsumeHeaderList(); + + const std::string body = "this is the body"; + std::unique_ptr<char[]> buf; + QuicByteCount header_length = + encoder_.SerializeDataFrameHeader(body.length(), &buf); + std::string header = std::string(buf.get(), header_length); + std::string data = HasFrameHeader() ? header + body : body; + + // Receive trailing headers. + SpdyHeaderBlock trailers_block; + trailers_block["key1"] = "value1"; + trailers_block["key2"] = "value2"; + trailers_block["key3"] = "value3"; + + QuicHeaderList trailers = ProcessHeaders(true, trailers_block); + + // The trailers should be decompressed, and readable from the stream. + EXPECT_TRUE(stream_->trailers_decompressed()); + + EXPECT_EQ(trailers_block, stream_->received_trailers()); + + // Consuming the trailers erases them from the stream. + stream_->MarkTrailersConsumed(); + EXPECT_TRUE(stream_->FinishedReadingTrailers()); + EXPECT_TRUE(stream_->IsDoneReading()); + + // Receive and consume body. + QuicStreamFrame frame(GetNthClientInitiatedBidirectionalId(0), /*fin=*/false, + 0, data); + stream_->OnStreamFrame(frame); + EXPECT_EQ(body, stream_->data()); + EXPECT_TRUE(stream_->IsDoneReading()); +} + TEST_P(QuicSpdyStreamTest, ReceivingTrailersWithoutFin) { // Test that received Trailers must always have the FIN set. Initialize(kShouldProcessData); + // In IETF QUIC, there is no such thing as FIN flag on HTTP/3 frames like the + // HEADERS frame. + if (VersionUsesQpack(GetParam().transport_version)) { + return; + } + // Receive initial headers. auto headers = AsHeaderList(headers_); stream_->OnStreamHeaderList(/*fin=*/false, @@ -1053,6 +1137,13 @@ // If body data are received with a FIN, no trailers should then arrive. Initialize(kShouldProcessData); + // If HEADERS frames are sent on the request/response stream, + // then the sequencer will block them from reaching QuicSpdyStream + // after the stream is closed. + if (VersionUsesQpack(GetParam().transport_version)) { + return; + } + // Receive initial headers without FIN set. ProcessHeaders(false, headers_); stream_->ConsumeHeaderList(); @@ -1101,6 +1192,12 @@ // to be sent on a stream. Initialize(kShouldProcessData); + if (VersionUsesQpack(GetParam().transport_version)) { + // In this case, TestStream::WriteHeadersImpl() does not prevent writes. + EXPECT_CALL(*session_, WritevData(stream_, stream_->id(), _, _, _)) + .Times(AtLeast(1)); + } + // Write the initial headers, without a FIN. EXPECT_CALL(*stream_, WriteHeadersMock(false)); stream_->WriteHeaders(SpdyHeaderBlock(), /*fin=*/false, nullptr); @@ -1118,6 +1215,12 @@ // peer contain the final offset field indicating last byte of data. Initialize(kShouldProcessData); + if (VersionUsesQpack(GetParam().transport_version)) { + // In this case, TestStream::WriteHeadersImpl() does not prevent writes. + EXPECT_CALL(*session_, WritevData(stream_, stream_->id(), _, _, _)) + .Times(AtLeast(1)); + } + // Write the initial headers. EXPECT_CALL(*stream_, WriteHeadersMock(false)); stream_->WriteHeaders(SpdyHeaderBlock(), /*fin=*/false, nullptr); @@ -1137,12 +1240,18 @@ // number of body bytes written (including queued bytes). SpdyHeaderBlock trailers; trailers["trailer key"] = "trailer value"; - SpdyHeaderBlock trailers_with_offset(trailers.Clone()); - trailers_with_offset[kFinalOffsetHeaderKey] = - QuicTextUtils::Uint64ToString(body.length() + header_length); + + SpdyHeaderBlock expected_trailers(trailers.Clone()); + // :final-offset pseudo-header is only added if trailers are sent + // on the headers stream. + if (!VersionUsesQpack(GetParam().transport_version)) { + expected_trailers[kFinalOffsetHeaderKey] = + QuicTextUtils::Uint64ToString(body.length() + header_length); + } + EXPECT_CALL(*stream_, WriteHeadersMock(true)); stream_->WriteTrailers(std::move(trailers), nullptr); - EXPECT_EQ(trailers_with_offset, stream_->saved_headers()); + EXPECT_EQ(expected_trailers, stream_->saved_headers()); } TEST_P(QuicSpdyStreamTest, WritingTrailersClosesWriteSide) { @@ -1150,12 +1259,16 @@ // (headers and body), that this closes the stream for writing. Initialize(kShouldProcessData); + // Expect data being written on the stream. In addition to that, headers are + // also written on the stream in case of IETF QUIC. + EXPECT_CALL(*session_, WritevData(stream_, stream_->id(), _, _, _)) + .Times(AtLeast(1)); + // Write the initial headers. EXPECT_CALL(*stream_, WriteHeadersMock(false)); stream_->WriteHeaders(SpdyHeaderBlock(), /*fin=*/false, nullptr); // Write non-zero body data. - EXPECT_CALL(*session_, WritevData(_, _, _, _, _)).Times(AtLeast(1)); const int kBodySize = 1 * 1024; // 1 kB stream_->WriteOrBufferBody(std::string(kBodySize, 'x'), false); EXPECT_EQ(0u, stream_->BufferedDataBytes()); @@ -1168,6 +1281,13 @@ } TEST_P(QuicSpdyStreamTest, WritingTrailersWithQueuedBytes) { + // This test exercises sending trailers on the headers stream while data is + // still queued on the response/request stream. In IETF QUIC, data and + // trailers are sent on the same stream, so this test does not apply. + if (VersionUsesQpack(GetParam().transport_version)) { + return; + } + // Test that the stream is not closed for writing when trailers are sent // while there are still body bytes queued. testing::InSequence seq; @@ -1202,7 +1322,10 @@ TEST_P(QuicSpdyStreamTest, WritingTrailersAfterFIN) { // EXPECT_QUIC_BUG tests are expensive so only run one instance of them. - if (GetParam() != AllSupportedVersions()[0]) { + // In IETF QUIC, there is no such thing as FIN flag on HTTP/3 frames like the + // HEADERS frame. That is version 99, which is element 0 of the array, so + // pick another element. + if (GetParam() != AllSupportedVersions()[1]) { return; } @@ -1554,6 +1677,65 @@ QuicSpdyStreamPeer::unacked_frame_headers_offsets(stream_).Empty()); } +TEST_P(QuicSpdyStreamTest, HeadersFrameOnRequestStream) { + if (!VersionUsesQpack(GetParam().transport_version)) { + return; + } + + Initialize(kShouldProcessData); + + // QPACK encoded header block with single header field "foo: bar". + std::string headers_frame_payload = + QuicTextUtils::HexDecode("00002a94e703626172"); + std::unique_ptr<char[]> headers_buffer; + QuicByteCount headers_frame_header_length = + encoder_.SerializeHeadersFrameHeader(headers_frame_payload.length(), + &headers_buffer); + QuicStringPiece headers_frame_header(headers_buffer.get(), + headers_frame_header_length); + + std::string data_frame_payload = "some data"; + std::unique_ptr<char[]> data_buffer; + QuicByteCount data_frame_header_length = encoder_.SerializeDataFrameHeader( + data_frame_payload.length(), &data_buffer); + QuicStringPiece data_frame_header(data_buffer.get(), + data_frame_header_length); + + // QPACK encoded header block with single header field + // "custom-key: custom-value". + std::string trailers_frame_payload = + QuicTextUtils::HexDecode("00002f0125a849e95ba97d7f8925a849e95bb8e8b4bf"); + std::unique_ptr<char[]> trailers_buffer; + QuicByteCount trailers_frame_header_length = + encoder_.SerializeHeadersFrameHeader(trailers_frame_payload.length(), + &trailers_buffer); + QuicStringPiece trailers_frame_header(trailers_buffer.get(), + trailers_frame_header_length); + + std::string stream_frame_payload = QuicStrCat( + headers_frame_header, headers_frame_payload, data_frame_header, + data_frame_payload, trailers_frame_header, trailers_frame_payload); + QuicStreamFrame frame(stream_->id(), false, 0, stream_frame_payload); + stream_->OnStreamFrame(frame); + + auto it = stream_->header_list().begin(); + ASSERT_TRUE(it != stream_->header_list().end()); + EXPECT_EQ("foo", it->first); + EXPECT_EQ("bar", it->second); + ++it; + EXPECT_TRUE(it == stream_->header_list().end()); + + // QuicSpdyStream only calls OnBodyAvailable() + // after the header list has been consumed. + EXPECT_EQ("", stream_->data()); + stream_->ConsumeHeaderList(); + EXPECT_EQ("some data", stream_->data()); + + const spdy::SpdyHeaderBlock& trailers = stream_->received_trailers(); + EXPECT_THAT(trailers, testing::ElementsAre( + testing::Pair("custom-key", "custom-value"))); +} + } // namespace } // namespace test } // namespace quic
diff --git a/quic/core/quic_versions.h b/quic/core/quic_versions.h index 1f6a431..02a0681 100644 --- a/quic/core/quic_versions.h +++ b/quic/core/quic_versions.h
@@ -343,13 +343,23 @@ return transport_version == QUIC_VERSION_99; } -// Returns true if QuicSpdySession instantiates a QPACK encoder and decoder. +// If true: +// * QuicSpdySession instantiates a QPACK encoder and decoder; +// * HEADERS frames (containing headers or trailers) are sent on +// request/response streams, compressed with QPACK; +// * trailers must not contain :final-offset key. +// If false: +// * HEADERS frames (containing headers or trailers) are sent on the headers +// stream, compressed with HPACK; +// * trailers must contain :final-offset key. +// // TODO(123528590): Implement the following features and gate them on this -// function as well, optionally renaming this function as appropriate. -// Send HEADERS on the request/response stream instead of the headers stream. -// Send PUSH_PROMISE on the request/response stream instead of headers stream. -// Send PRIORITY on the request/response stream instead of the headers stream. -// Do not instantiate the headers stream object. +// function as well, optionally renaming this function as appropriate: +// * send PUSH_PROMISE frames on the request/response stream instead of the +// headers stream; +// * send PRIORITY frames on the request/response stream instead of the headers +// stream; +// * do not instantiate the headers stream object. QUIC_EXPORT_PRIVATE inline bool VersionUsesQpack( QuicTransportVersion transport_version) { const bool uses_qpack = (transport_version == QUIC_VERSION_99);
diff --git a/quic/tools/quic_simple_server_session_test.cc b/quic/tools/quic_simple_server_session_test.cc index aa9104a..e08af9a 100644 --- a/quic/tools/quic_simple_server_session_test.cc +++ b/quic/tools/quic_simple_server_session_test.cc
@@ -46,7 +46,12 @@ namespace quic { namespace test { namespace { + typedef QuicSimpleServerSession::PromisedStreamInfo PromisedStreamInfo; + +const QuicByteCount kHeadersFrameHeaderLength = 2; +const QuicByteCount kHeadersFramePayloadLength = 9; + } // namespace class QuicSimpleServerSessionPeer { @@ -643,6 +648,16 @@ // Since flow control window is smaller than response body, not the // whole body will be sent. QuicStreamOffset offset = 0; + if (VersionUsesQpack(connection_->transport_version())) { + EXPECT_CALL(*connection_, + SendStreamData(stream_id, kHeadersFrameHeaderLength, + offset, NO_FIN)); + offset += kHeadersFrameHeaderLength; + EXPECT_CALL(*connection_, + SendStreamData(stream_id, kHeadersFramePayloadLength, + offset, NO_FIN)); + offset += kHeadersFramePayloadLength; + } if (VersionHasDataFrameHeader(connection_->transport_version())) { EXPECT_CALL(*connection_, SendStreamData(stream_id, data_frame_header_length, @@ -661,11 +676,13 @@ return data_frame_header_length; } - void ConsumeHeadersStreamData() { - QuicStreamId headers_stream_id = - QuicUtils::GetHeadersStreamId(connection_->transport_version()); - EXPECT_CALL(*connection_, SendStreamData(headers_stream_id, _, _, _)) - .Times(AtLeast(1)); + void MaybeConsumeHeadersStreamData() { + if (!VersionUsesQpack(connection_->transport_version())) { + QuicStreamId headers_stream_id = + QuicUtils::GetHeadersStreamId(connection_->transport_version()); + EXPECT_CALL(*connection_, SendStreamData(headers_stream_id, _, _, _)) + .Times(AtLeast(1)); + } } }; @@ -677,7 +694,7 @@ // PUSH_PROMISE's will be sent out and only kMaxStreamsForTest streams will be // opened and send push response. TEST_P(QuicSimpleServerSessionServerPushTest, TestPromisePushResources) { - ConsumeHeadersStreamData(); + MaybeConsumeHeadersStreamData(); size_t num_resources = kMaxStreamsForTest + 5; PromisePushResources(num_resources); EXPECT_EQ(kMaxStreamsForTest, session_->GetNumOpenOutgoingStreams()); @@ -687,7 +704,7 @@ // draining, a queued promised stream will become open and send push response. TEST_P(QuicSimpleServerSessionServerPushTest, HandlePromisedPushRequestsAfterStreamDraining) { - ConsumeHeadersStreamData(); + MaybeConsumeHeadersStreamData(); size_t num_resources = kMaxStreamsForTest + 1; QuicByteCount data_frame_header_length = PromisePushResources(num_resources); QuicStreamId next_out_going_stream_id = @@ -696,6 +713,16 @@ // After an open stream is marked draining, a new stream is expected to be // created and a response sent on the stream. QuicStreamOffset offset = 0; + if (VersionUsesQpack(connection_->transport_version())) { + EXPECT_CALL(*connection_, + SendStreamData(next_out_going_stream_id, + kHeadersFrameHeaderLength, offset, NO_FIN)); + offset += kHeadersFrameHeaderLength; + EXPECT_CALL(*connection_, + SendStreamData(next_out_going_stream_id, + kHeadersFramePayloadLength, offset, NO_FIN)); + offset += kHeadersFramePayloadLength; + } if (VersionHasDataFrameHeader(connection_->transport_version())) { EXPECT_CALL(*connection_, SendStreamData(next_out_going_stream_id, @@ -728,7 +755,7 @@ // prevent a promised resource to be send out. TEST_P(QuicSimpleServerSessionServerPushTest, ResetPromisedStreamToCancelServerPush) { - ConsumeHeadersStreamData(); + MaybeConsumeHeadersStreamData(); // Having two extra resources to be send later. One of them will be reset, so // when opened stream become close, only one will become open. @@ -763,6 +790,16 @@ GetNthServerInitiatedUnidirectionalId(kMaxStreamsForTest); InSequence s; QuicStreamOffset offset = 0; + if (VersionUsesQpack(connection_->transport_version())) { + EXPECT_CALL(*connection_, + SendStreamData(stream_not_reset, kHeadersFrameHeaderLength, + offset, NO_FIN)); + offset += kHeadersFrameHeaderLength; + EXPECT_CALL(*connection_, + SendStreamData(stream_not_reset, kHeadersFramePayloadLength, + offset, NO_FIN)); + offset += kHeadersFramePayloadLength; + } if (VersionHasDataFrameHeader(connection_->transport_version())) { EXPECT_CALL(*connection_, SendStreamData(stream_not_reset, data_frame_header_length, @@ -791,7 +828,7 @@ // the queue to be send out. TEST_P(QuicSimpleServerSessionServerPushTest, CloseStreamToHandleMorePromisedStream) { - ConsumeHeadersStreamData(); + MaybeConsumeHeadersStreamData(); size_t num_resources = kMaxStreamsForTest + 1; if (IsVersion99()) { // V99 will send out a stream-id-blocked frame when the we desired to exceed @@ -816,6 +853,16 @@ OnStreamReset(stream_got_reset, QUIC_RST_ACKNOWLEDGEMENT)); } QuicStreamOffset offset = 0; + if (VersionUsesQpack(connection_->transport_version())) { + EXPECT_CALL(*connection_, + SendStreamData(stream_to_open, kHeadersFrameHeaderLength, + offset, NO_FIN)); + offset += kHeadersFrameHeaderLength; + EXPECT_CALL(*connection_, + SendStreamData(stream_to_open, kHeadersFramePayloadLength, + offset, NO_FIN)); + offset += kHeadersFramePayloadLength; + } if (VersionHasDataFrameHeader(connection_->transport_version())) { EXPECT_CALL(*connection_, SendStreamData(stream_to_open, data_frame_header_length, offset,