Add support for parsing and processing the HTTP/3 Priority header in QUIC. Default disabled. PiperOrigin-RevId: 961212830
diff --git a/quiche/quic/core/http/http_constants.cc b/quiche/quic/core/http/http_constants.cc index 7dbb585..eb43a2f 100644 --- a/quiche/quic/core/http/http_constants.cc +++ b/quiche/quic/core/http/http_constants.cc
@@ -30,6 +30,7 @@ } ABSL_CONST_INIT const absl::string_view kUserAgentHeaderName = "user-agent"; +ABSL_CONST_INIT const absl::string_view kPriorityHeaderName = "priority"; #undef RETURN_STRING_LITERAL // undef for jumbo builds
diff --git a/quiche/quic/core/http/http_constants.h b/quiche/quic/core/http/http_constants.h index 11e53be..21ecf26 100644 --- a/quiche/quic/core/http/http_constants.h +++ b/quiche/quic/core/http/http_constants.h
@@ -68,6 +68,8 @@ ABSL_CONST_INIT QUICHE_EXPORT extern const absl::string_view kUserAgentHeaderName; +ABSL_CONST_INIT QUICHE_EXPORT extern const absl::string_view + kPriorityHeaderName; } // namespace quic
diff --git a/quiche/quic/core/http/quic_spdy_session.h b/quiche/quic/core/http/quic_spdy_session.h index 46cee7b..5e96a4d 100644 --- a/quiche/quic/core/http/quic_spdy_session.h +++ b/quiche/quic/core/http/quic_spdy_session.h
@@ -331,6 +331,11 @@ bool allow_extended_connect() const { return allow_extended_connect_; } + bool process_priority_header() const { return process_priority_header_; } + void set_process_priority_header(bool process_priority_header) { + process_priority_header_ = process_priority_header; + } + // Returns true if the session has active request streams. bool HasActiveRequestStreams() const; @@ -745,6 +750,9 @@ // server cannot initiate WebTransport sessions. absl::flat_hash_map<WebTransportHttp3Version, QuicStreamCount> max_webtransport_sessions_; + + // Whether to process priority headers from the client. + bool process_priority_header_ = false; }; } // namespace quic
diff --git a/quiche/quic/core/http/quic_spdy_session_test.cc b/quiche/quic/core/http/quic_spdy_session_test.cc index a1e8107..a3509a6 100644 --- a/quiche/quic/core/http/quic_spdy_session_test.cc +++ b/quiche/quic/core/http/quic_spdy_session_test.cc
@@ -2398,6 +2398,282 @@ stream2->priority_source()); } +TEST_P(QuicSpdySessionTestServer, OnPriorityHeader) { + Initialize(); + session_->set_process_priority_header(true); + if (!VersionIsIetfQuic(transport_version())) { + return; + } + + CompleteHandshake(); + + const QuicStreamId stream_id = GetNthClientInitiatedBidirectionalId(0); + TestStream* stream = session_->CreateIncomingStream(stream_id); + + // Send request headers with Priority header. + quiche::HttpHeaderBlock headers; + headers[":path"] = "/foo"; + headers[":authority"] = "www.google.com"; + headers[":method"] = "GET"; + headers[":scheme"] = "https"; + headers["priority"] = "u=2, i"; + + QuicHeaderList header_list = AsHeaderList(headers); + stream->OnStreamHeaderList( + /* fin = */ false, header_list.uncompressed_header_bytes(), header_list); + + EXPECT_EQ(QuicStreamPriority(HttpStreamPriority{2u, true}), + stream->priority()); + EXPECT_EQ(PrioritySource::SET_BY_REQUEST_HEADER, stream->priority_source()); +} + +TEST_P(QuicSpdySessionTestServer, OnPriorityHeaderAndPriorityUpdateFrame) { + Initialize(); + session_->set_process_priority_header(true); + + if (!VersionIsIetfQuic(transport_version())) { + return; + } + + StrictMock<MockHttp3DebugVisitor> debug_visitor; + session_->set_debug_visitor(&debug_visitor); + EXPECT_CALL(debug_visitor, OnSettingsFrameSent(_)); + CompleteHandshake(); + + // Create control stream and send SETTINGS. + QuicStreamId receive_control_stream_id = + GetNthClientInitiatedUnidirectionalStreamId(transport_version(), 3); + char type[] = {kControlStream}; + absl::string_view stream_type(type, 1); + QuicStreamOffset offset = 0; + QuicStreamFrame data1(receive_control_stream_id, false, offset, stream_type); + offset += stream_type.length(); + EXPECT_CALL(debug_visitor, + OnPeerControlStreamCreated(receive_control_stream_id)); + session_->OnStreamFrame(data1); + std::string serialized_settings = HttpEncoder::SerializeSettingsFrame({}); + QuicStreamFrame data2(receive_control_stream_id, false, offset, + serialized_settings); + offset += serialized_settings.length(); + EXPECT_CALL(debug_visitor, OnSettingsFrameReceived(_)); + session_->OnStreamFrame(data2); + + const QuicStreamId stream_id = GetNthClientInitiatedBidirectionalId(0); + + // 1. PRIORITY_UPDATE frame arrives FIRST (before stream creation). + PriorityUpdateFrame priority_update{stream_id, "u=5, i"}; + std::string serialized_priority_update = + HttpEncoder::SerializePriorityUpdateFrame(priority_update); + QuicStreamFrame stream_frame3(receive_control_stream_id, + /* fin = */ false, offset, + serialized_priority_update); + EXPECT_CALL(debug_visitor, OnPriorityUpdateFrameReceived(priority_update)); + session_->OnStreamFrame(stream_frame3); + + // 2. Stream is created. Priority from frame (u=5, i) is applied. + TestStream* stream = session_->CreateIncomingStream(stream_id); + EXPECT_EQ(QuicStreamPriority(HttpStreamPriority{5u, true}), + stream->priority()); + EXPECT_EQ(PrioritySource::SET_BY_PRIORITY_UPDATE, stream->priority_source()); + + // 3. HEADERS frame with Priority header (u=2) arrives SECOND. + quiche::HttpHeaderBlock headers; + headers[":path"] = "/foo"; + headers[":authority"] = "www.google.com"; + headers[":method"] = "GET"; + headers[":scheme"] = "https"; + headers["priority"] = "u=2"; // Different priority! + + QuicHeaderList header_list = AsHeaderList(headers); + // It should IGNORE the Priority header because priority was set by frame! + stream->OnStreamHeaderList( + /* fin = */ false, header_list.uncompressed_header_bytes(), header_list); + + // Priority should STILL be u=5, i! + EXPECT_EQ(QuicStreamPriority(HttpStreamPriority{5u, true}), + stream->priority()); + EXPECT_EQ(PrioritySource::SET_BY_PRIORITY_UPDATE, stream->priority_source()); +} + +TEST_P(QuicSpdySessionTestServer, + OnPriorityUpdateFrameArrivesAfterStreamCreationButBeforeHeaders) { + Initialize(); + session_->set_process_priority_header(true); + if (!VersionIsIetfQuic(transport_version())) { + return; + } + + StrictMock<MockHttp3DebugVisitor> debug_visitor; + session_->set_debug_visitor(&debug_visitor); + EXPECT_CALL(debug_visitor, OnSettingsFrameSent(_)); + CompleteHandshake(); + + // Create control stream and send SETTINGS. + QuicStreamId receive_control_stream_id = + GetNthClientInitiatedUnidirectionalStreamId(transport_version(), 3); + char type[] = {kControlStream}; + absl::string_view stream_type(type, 1); + QuicStreamOffset offset = 0; + QuicStreamFrame data1(receive_control_stream_id, false, offset, stream_type); + offset += stream_type.length(); + EXPECT_CALL(debug_visitor, + OnPeerControlStreamCreated(receive_control_stream_id)); + session_->OnStreamFrame(data1); + std::string serialized_settings = HttpEncoder::SerializeSettingsFrame({}); + QuicStreamFrame data2(receive_control_stream_id, false, offset, + serialized_settings); + offset += serialized_settings.length(); + EXPECT_CALL(debug_visitor, OnSettingsFrameReceived(_)); + session_->OnStreamFrame(data2); + + const QuicStreamId stream_id = GetNthClientInitiatedBidirectionalId(0); + + // 1. Stream is created FIRST. It gets default priority. + TestStream* stream = session_->CreateIncomingStream(stream_id); + EXPECT_EQ(QuicStreamPriority( + HttpStreamPriority{HttpStreamPriority::kDefaultUrgency, + HttpStreamPriority::kDefaultIncremental}), + stream->priority()); + EXPECT_EQ(PrioritySource::NOT_SET, stream->priority_source()); + + // 2. PRIORITY_UPDATE frame arrives SECOND. It updates priority to u=1. + PriorityUpdateFrame priority_update{stream_id, "u=1"}; + std::string serialized_priority_update = + HttpEncoder::SerializePriorityUpdateFrame(priority_update); + QuicStreamFrame stream_frame3(receive_control_stream_id, + /* fin = */ false, offset, + serialized_priority_update); + EXPECT_CALL(debug_visitor, OnPriorityUpdateFrameReceived(priority_update)); + session_->OnStreamFrame(stream_frame3); + EXPECT_EQ(QuicStreamPriority(HttpStreamPriority{ + 1u, HttpStreamPriority::kDefaultIncremental}), + stream->priority()); + EXPECT_EQ(PrioritySource::SET_BY_PRIORITY_UPDATE, stream->priority_source()); + + // 3. HEADERS frame with Priority header (u=2) arrives THIRD. + quiche::HttpHeaderBlock headers; + headers[":path"] = "/foo"; + headers[":authority"] = "www.google.com"; + headers[":method"] = "GET"; + headers[":scheme"] = "https"; + headers["priority"] = "u=2"; // Different priority! + + QuicHeaderList header_list = AsHeaderList(headers); + // It should IGNORE the Priority header because priority was set by frame! + stream->OnStreamHeaderList( + /* fin = */ false, header_list.uncompressed_header_bytes(), header_list); + + // Priority should STILL be u=1! + EXPECT_EQ(QuicStreamPriority(HttpStreamPriority{ + 1u, HttpStreamPriority::kDefaultIncremental}), + stream->priority()); + EXPECT_EQ(PrioritySource::SET_BY_PRIORITY_UPDATE, stream->priority_source()); +} + +TEST_P(QuicSpdySessionTestServer, + OnPriorityHeaderArrivesThenPriorityUpdateFrame) { + Initialize(); + session_->set_process_priority_header(true); + if (!VersionIsIetfQuic(transport_version())) { + return; + } + + StrictMock<MockHttp3DebugVisitor> debug_visitor; + session_->set_debug_visitor(&debug_visitor); + EXPECT_CALL(debug_visitor, OnSettingsFrameSent(_)); + CompleteHandshake(); + + // Create control stream and send SETTINGS. + QuicStreamId receive_control_stream_id = + GetNthClientInitiatedUnidirectionalStreamId(transport_version(), 3); + char type[] = {kControlStream}; + absl::string_view stream_type(type, 1); + QuicStreamOffset offset = 0; + QuicStreamFrame data1(receive_control_stream_id, false, offset, stream_type); + offset += stream_type.length(); + EXPECT_CALL(debug_visitor, + OnPeerControlStreamCreated(receive_control_stream_id)); + session_->OnStreamFrame(data1); + std::string serialized_settings = HttpEncoder::SerializeSettingsFrame({}); + QuicStreamFrame data2(receive_control_stream_id, false, offset, + serialized_settings); + offset += serialized_settings.length(); + EXPECT_CALL(debug_visitor, OnSettingsFrameReceived(_)); + session_->OnStreamFrame(data2); + + const QuicStreamId stream_id = GetNthClientInitiatedBidirectionalId(0); + + // 1. Stream is created. + TestStream* stream = session_->CreateIncomingStream(stream_id); + EXPECT_EQ(PrioritySource::NOT_SET, stream->priority_source()); + + // 2. HEADERS frame with Priority header (u=2) arrives FIRST. + quiche::HttpHeaderBlock headers; + headers[":path"] = "/foo"; + headers[":authority"] = "www.google.com"; + headers[":method"] = "GET"; + headers[":scheme"] = "https"; + headers["priority"] = "u=2"; + + QuicHeaderList header_list = AsHeaderList(headers); + stream->OnStreamHeaderList( + /* fin = */ false, header_list.uncompressed_header_bytes(), header_list); + + // Priority should be u=2! + EXPECT_EQ(QuicStreamPriority(HttpStreamPriority{ + 2u, HttpStreamPriority::kDefaultIncremental}), + stream->priority()); + EXPECT_EQ(PrioritySource::SET_BY_REQUEST_HEADER, stream->priority_source()); + + // 3. PRIORITY_UPDATE frame arrives SECOND. It updates priority to u=5, i. + PriorityUpdateFrame priority_update{stream_id, "u=5, i"}; + std::string serialized_priority_update = + HttpEncoder::SerializePriorityUpdateFrame(priority_update); + QuicStreamFrame stream_frame3(receive_control_stream_id, + /* fin = */ false, offset, + serialized_priority_update); + EXPECT_CALL(debug_visitor, OnPriorityUpdateFrameReceived(priority_update)); + session_->OnStreamFrame(stream_frame3); + + // Priority should be OVERRIDDEN to u=5, i! + EXPECT_EQ(QuicStreamPriority(HttpStreamPriority{5u, true}), + stream->priority()); + EXPECT_EQ(PrioritySource::SET_BY_PRIORITY_UPDATE, stream->priority_source()); +} + +TEST_P(QuicSpdySessionTestServer, OnPriorityHeaderDisabled) { + Initialize(); + session_->set_process_priority_header(false); + if (!VersionIsIetfQuic(transport_version())) { + return; + } + + CompleteHandshake(); + + const QuicStreamId stream_id = GetNthClientInitiatedBidirectionalId(0); + TestStream* stream = session_->CreateIncomingStream(stream_id); + + // Send request headers with Priority header. + quiche::HttpHeaderBlock headers; + headers[":path"] = "/foo"; + headers[":authority"] = "www.google.com"; + headers[":method"] = "GET"; + headers[":scheme"] = "https"; + headers["priority"] = "u=2, i"; + + QuicHeaderList header_list = AsHeaderList(headers); + stream->OnStreamHeaderList( + /* fin = */ false, header_list.uncompressed_header_bytes(), header_list); + + // Priority should remain the DEFAULT (u=3, no incremental) because flag is + // false! + EXPECT_EQ(QuicStreamPriority( + HttpStreamPriority{HttpStreamPriority::kDefaultUrgency, + HttpStreamPriority::kDefaultIncremental}), + stream->priority()); + EXPECT_EQ(PrioritySource::NOT_SET, stream->priority_source()); +} + TEST_P(QuicSpdySessionTestServer, OnInvalidPriorityUpdateFrame) { Initialize(); if (!VersionIsIetfQuic(transport_version())) {
diff --git a/quiche/quic/core/http/quic_spdy_stream.cc b/quiche/quic/core/http/quic_spdy_stream.cc index f58b650..245d6ca 100644 --- a/quiche/quic/core/http/quic_spdy_stream.cc +++ b/quiche/quic/core/http/quic_spdy_stream.cc
@@ -678,6 +678,7 @@ if (!header_too_large) { MaybeProcessReceivedWebTransportHeaders(); + MaybeProcessPriorityHeader(); } if (VersionIsIetfQuic(transport_version())) { @@ -1443,6 +1444,40 @@ std::make_unique<WebTransportHttp3>(spdy_session_, this, id()); } +void QuicSpdyStream::MaybeProcessPriorityHeader() { + if (!spdy_session_->process_priority_header()) { + return; + } + if (!VersionIsIetfQuic(transport_version())) { + return; + } + if (session()->perspective() != Perspective::IS_SERVER) { + return; + } + if (priority_source() == PrioritySource::SET_BY_PRIORITY_UPDATE) { + return; + } + std::string priority_value; + for (const auto& [header_name, header_value] : header_list_) { + if (quiche::QuicheTextUtils::ToLower(header_name) == kPriorityHeaderName) { + priority_value = header_value; + break; + } + } + if (priority_value.empty()) { + return; + } + std::optional<HttpStreamPriority> priority = + ParsePriorityFieldValue(priority_value); + if (priority.has_value()) { + SetPriority(QuicStreamPriority(*priority)); + set_priority_source(PrioritySource::SET_BY_REQUEST_HEADER); + } else { + QUIC_DVLOG(1) << "Stream " << id() + << " ignoring malformed Priority header: " << priority_value; + } +} + void QuicSpdyStream::MaybeProcessSentWebTransportHeaders( quiche::HttpHeaderBlock& headers) { if (!spdy_session_->SupportsWebTransport()) {
diff --git a/quiche/quic/core/http/quic_spdy_stream.h b/quiche/quic/core/http/quic_spdy_stream.h index 5adb54e..54d58aa 100644 --- a/quiche/quic/core/http/quic_spdy_stream.h +++ b/quiche/quic/core/http/quic_spdy_stream.h
@@ -474,6 +474,7 @@ void MaybeProcessSentWebTransportHeaders(quiche::HttpHeaderBlock& headers); void MaybeProcessReceivedWebTransportHeaders(); + void MaybeProcessPriorityHeader(); // Writes HTTP/3 DATA frame header. If |force_write| is true, use // WriteOrBufferData if send buffer cannot accomodate the header + data.