Close the connection with error QUIC_HTTP_INVALID_FRAME_SEQUENCE_ON_SPDY_STREAM if FIN is received before finishing receiving the whole HTTP headers.

Protected by FLAGS_quic_reloadable_flag_quic_fin_before_completed_http_headers.

PiperOrigin-RevId: 698480775
diff --git a/quiche/common/quiche_feature_flags_list.h b/quiche/common/quiche_feature_flags_list.h
index 23c44df..f3df34d 100755
--- a/quiche/common/quiche_feature_flags_list.h
+++ b/quiche/common/quiche_feature_flags_list.h
@@ -36,6 +36,7 @@
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_enable_new_chaos_protector, false, false, "If true, use new refactored QuicChaosProtector implementation.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_enable_server_on_wire_ping, true, true, "If true, enable server retransmittable on wire PING.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_enable_version_rfcv2, false, false, "When true, support RFC9369.")
+QUICHE_FLAG(bool, quiche_reloadable_flag_quic_fin_before_completed_http_headers, false, true, "If true, close the connection with error if FIN is received before finish receiving the whole HTTP headers.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_fix_timeouts, true, true, "If true, postpone setting handshake timeout to infinite to handshake complete.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_gfe_allow_alps_new_codepoint, true, true, "If true, allow quic to use new ALPS codepoint to negotiate during handshake for H3 if client sends new ALPS codepoint.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_ignore_gquic_probing, true, true, "If true, QUIC server will not respond to gQUIC probing packet(PING + PADDING) but treat it as a regular packet.")
diff --git a/quiche/quic/core/http/end_to_end_test.cc b/quiche/quic/core/http/end_to_end_test.cc
index 2ee4d33..a2043dd 100644
--- a/quiche/quic/core/http/end_to_end_test.cc
+++ b/quiche/quic/core/http/end_to_end_test.cc
@@ -8141,6 +8141,32 @@
   EXPECT_EQ("", client_->SendSynchronousRequest("/foo"));
   EXPECT_THAT(client_->connection_error(), IsError(QUIC_PUBLIC_RESET));
 }
+
+TEST_P(EndToEndTest, EmptyResponseWithFin) {
+  ASSERT_TRUE(Initialize());
+  if (!version_.HasIetfQuicFrames()) {
+    return;
+  }
+  memory_cache_backend_.AddSpecialResponse(
+      server_hostname_, "/empty_response_with_fin",
+      QuicBackendResponse::EMPTY_PAYLOAD_WITH_FIN);
+
+  quiche::HttpHeaderBlock headers;
+  headers[":scheme"] = "https";
+  headers[":authority"] = server_hostname_;
+  headers[":method"] = "GET";
+  headers[":path"] = "/empty_response_with_fin";
+  client_->SendMessage(headers, "", /*fin=*/true);
+  client_->WaitForResponseForMs(100);
+  if (GetQuicReloadableFlag(quic_fin_before_completed_http_headers)) {
+    EXPECT_THAT(client_->connection_error(),
+                IsError(QUIC_HTTP_INVALID_FRAME_SEQUENCE_ON_SPDY_STREAM));
+  } else {
+    EXPECT_FALSE(client_->response_headers_complete());
+    EXPECT_FALSE(client_->response_complete());
+  }
+}
+
 }  // namespace
 }  // namespace test
 }  // namespace quic
diff --git a/quiche/quic/core/http/quic_spdy_stream.cc b/quiche/quic/core/http/quic_spdy_stream.cc
index 2e649ab..ee9be7b 100644
--- a/quiche/quic/core/http/quic_spdy_stream.cc
+++ b/quiche/quic/core/http/quic_spdy_stream.cc
@@ -822,9 +822,6 @@
   if (!VersionUsesHttp3(transport_version())) {
     // Sequencer must be blocked until headers are consumed.
     QUICHE_DCHECK(FinishedReadingHeaders());
-  }
-
-  if (!VersionUsesHttp3(transport_version())) {
     HandleBodyAvailable();
     return;
   }
@@ -880,6 +877,17 @@
     }
   }
 
+  if (GetQuicReloadableFlag(quic_fin_before_completed_http_headers)) {
+    if (sequencer()->IsClosed() && !headers_decompressed_) {
+      QUIC_RELOADABLE_FLAG_COUNT_N(quic_fin_before_completed_http_headers, 1,
+                                   2);
+      OnUnrecoverableError(
+          QUIC_HTTP_INVALID_FRAME_SEQUENCE_ON_SPDY_STREAM,
+          "Received FIN before finishing receiving HTTP headers.");
+      return;
+    }
+    QUIC_RELOADABLE_FLAG_COUNT_N(quic_fin_before_completed_http_headers, 2, 2);
+  }
   // Do not call HandleBodyAvailable() until headers are consumed.
   if (!FinishedReadingHeaders()) {
     return;
diff --git a/quiche/quic/core/http/quic_spdy_stream_test.cc b/quiche/quic/core/http/quic_spdy_stream_test.cc
index 1ca8721..05ca93e 100644
--- a/quiche/quic/core/http/quic_spdy_stream_test.cc
+++ b/quiche/quic/core/http/quic_spdy_stream_test.cc
@@ -2142,6 +2142,46 @@
   EXPECT_FALSE(stream_->HasBytesToRead());
 }
 
+TEST_P(QuicSpdyStreamTest, IncompleteHeadersWithFin) {
+  SetQuicReloadableFlag(quic_fin_before_completed_http_headers, true);
+  if (!UsesHttp3()) {
+    return;
+  }
+
+  Initialize(!kShouldProcessData);
+
+  std::string headers = HeadersFrame({std::make_pair("foo", "bar")});
+  std::string partial_headers = headers.substr(0, headers.length() - 2);
+  EXPECT_FALSE(partial_headers.empty());
+  // Receive the first three bytes of the headers frame with FIN.
+  QuicStreamFrame frame(stream_->id(), true, 0, partial_headers);
+  EXPECT_CALL(
+      *connection_,
+      CloseConnection(
+          QUIC_HTTP_INVALID_FRAME_SEQUENCE_ON_SPDY_STREAM,
+          MatchesRegex("Received FIN before finishing receiving HTTP headers."),
+          ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET));
+  stream_->OnStreamFrame(frame);
+}
+
+TEST_P(QuicSpdyStreamTest, EmptyStreamFrameWithFin) {
+  SetQuicReloadableFlag(quic_fin_before_completed_http_headers, true);
+  if (!UsesHttp3()) {
+    return;
+  }
+  Initialize(!kShouldProcessData);
+
+  // Receive the first three bytes of the headers frame with FIN.
+  QuicStreamFrame frame(stream_->id(), true, 0, 0);
+  EXPECT_CALL(
+      *connection_,
+      CloseConnection(
+          QUIC_HTTP_INVALID_FRAME_SEQUENCE_ON_SPDY_STREAM,
+          MatchesRegex("Received FIN before finishing receiving HTTP headers."),
+          ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET));
+  stream_->OnStreamFrame(frame);
+}
+
 // The test stream will receive a stream frame containing malformed headers and
 // normal body. Make sure the http decoder stops processing body after the
 // connection shuts down.
diff --git a/quiche/quic/tools/quic_backend_response.h b/quiche/quic/tools/quic_backend_response.h
index 5564a6d..55de71d 100644
--- a/quiche/quic/tools/quic_backend_response.h
+++ b/quiche/quic/tools/quic_backend_response.h
@@ -25,8 +25,9 @@
     INCOMPLETE_RESPONSE,   // The server will act as if there is a non-empty
                            // trailer but it will not be sent, as a result, FIN
                            // will not be sent too.
-    GENERATE_BYTES         // Sends a response with a length equal to the number
+    GENERATE_BYTES,        // Sends a response with a length equal to the number
                            // of bytes in the URL path.
+    EMPTY_PAYLOAD_WITH_FIN,  // Sends an empty STREAM_FRAME with FIN.
   };
   QuicBackendResponse();
 
diff --git a/quiche/quic/tools/quic_simple_server_stream.cc b/quiche/quic/tools/quic_simple_server_stream.cc
index a805b47..0c53857 100644
--- a/quiche/quic/tools/quic_simple_server_stream.cc
+++ b/quiche/quic/tools/quic_simple_server_stream.cc
@@ -303,6 +303,15 @@
     return;
   }
 
+  if (response->response_type() ==
+      QuicBackendResponse::EMPTY_PAYLOAD_WITH_FIN) {
+    // Send an empty payload with FIN without any response headers or body.
+    absl::InlinedVector<quiche::QuicheMemSlice, 4> quic_slices;
+    absl::Span<quiche::QuicheMemSlice> span(quic_slices);
+    WriteMemSlices(std::move(span), true);
+    return;
+  }
+
   // Examing response status, if it was not pure integer as typical h2
   // response status, send error response. Notice that
   // QuicHttpResponseCache push urls are strictly authority + path only,
diff --git a/quiche/quic/tools/quic_simple_server_stream_test.cc b/quiche/quic/tools/quic_simple_server_stream_test.cc
index dab99dd..e1d5856 100644
--- a/quiche/quic/tools/quic_simple_server_stream_test.cc
+++ b/quiche/quic/tools/quic_simple_server_stream_test.cc
@@ -740,6 +740,12 @@
   absl::string_view data(arr, ABSL_ARRAYSIZE(arr));
   QuicStreamFrame frame(stream_->id(), true, 0, data);
   // Verify that we don't crash when we get a invalid headers in stream frame.
+  if (GetQuicReloadableFlag(quic_fin_before_completed_http_headers) &&
+      UsesHttp3()) {
+    EXPECT_CALL(
+        *connection_,
+        CloseConnection(QUIC_HTTP_INVALID_FRAME_SEQUENCE_ON_SPDY_STREAM, _, _));
+  }
   stream_->OnStreamFrame(frame);
 }