Disable capsule parsing for failed requests

Otherwise we were incorrectly parsing the body as capsules for rejected MASQUE requests.

PiperOrigin-RevId: 640970762
diff --git a/quiche/quic/core/http/quic_spdy_client_stream.cc b/quiche/quic/core/http/quic_spdy_client_stream.cc
index d173277..323fa9b 100644
--- a/quiche/quic/core/http/quic_spdy_client_stream.cc
+++ b/quiche/quic/core/http/quic_spdy_client_stream.cc
@@ -107,10 +107,7 @@
     if (!web_transport()->ready()) {
       // The request was rejected by WebTransport, typically due to not having a
       // 2xx status.  The reason we're using Reset() here rather than closing
-      // cleanly is that even if the server attempts to send us any form of body
-      // with a 4xx request, we've already set up the capsule parser, and we
-      // don't have any way to process anything from the response body in
-      // question.
+      // cleanly is to avoid having to process the response body.
       Reset(QUIC_STREAM_CANCELLED);
       return;
     }
@@ -120,6 +117,10 @@
     return;
   }
 
+  if (uses_capsules() && (response_code_ < 200 || response_code_ >= 300)) {
+    capsules_failed_ = true;
+  }
+
   ConsumeHeaderList();
   QUIC_DVLOG(1) << "headers complete for stream " << id();
 }
diff --git a/quiche/quic/core/http/quic_spdy_client_stream.h b/quiche/quic/core/http/quic_spdy_client_stream.h
index 001f312..bbe922a 100644
--- a/quiche/quic/core/http/quic_spdy_client_stream.h
+++ b/quiche/quic/core/http/quic_spdy_client_stream.h
@@ -90,6 +90,10 @@
   // response_header_. Returns false on error.
   virtual bool ParseAndValidateStatusCode();
 
+  bool uses_capsules() const override {
+    return QuicSpdyStream::uses_capsules() && !capsules_failed_;
+  }
+
  private:
   // The parsed headers received from the server.
   spdy::Http2HeaderBlock response_headers_;
@@ -97,6 +101,7 @@
   // The parsed content-length, or -1 if none is specified.
   int64_t content_length_;
   int response_code_;
+  bool capsules_failed_ = false;
   std::string data_;
   size_t header_bytes_read_;
   size_t header_bytes_written_;
diff --git a/quiche/quic/core/http/quic_spdy_client_stream_test.cc b/quiche/quic/core/http/quic_spdy_client_stream_test.cc
index ed03be2..ce39e5b 100644
--- a/quiche/quic/core/http/quic_spdy_client_stream_test.cc
+++ b/quiche/quic/core/http/quic_spdy_client_stream_test.cc
@@ -24,6 +24,7 @@
 
 using spdy::Http2HeaderBlock;
 using testing::_;
+using testing::ElementsAre;
 using testing::StrictMock;
 
 namespace quic {
@@ -390,6 +391,65 @@
   EXPECT_TRUE(stream_->reading_stopped());
 }
 
+TEST_P(QuicSpdyClientStreamTest, Capsules) {
+  if (!VersionUsesHttp3(connection_->transport_version())) {
+    return;
+  }
+  SavingHttp3DatagramVisitor h3_datagram_visitor;
+  stream_->RegisterHttp3DatagramVisitor(&h3_datagram_visitor);
+  headers_.erase("content-length");
+  auto headers = AsHeaderList(headers_);
+  stream_->OnStreamHeaderList(false, headers.uncompressed_header_bytes(),
+                              headers);
+  std::string capsule_data = {0, 6, 1, 2, 3, 4, 5, 6, 0x17, 4, 1, 2, 3, 4};
+  quiche::QuicheBuffer data_frame_header =
+      HttpEncoder::SerializeDataFrameHeader(
+          capsule_data.length(), quiche::SimpleBufferAllocator::Get());
+  std::string stream_data =
+      absl::StrCat(data_frame_header.AsStringView(), capsule_data);
+  stream_->OnStreamFrame(
+      QuicStreamFrame(stream_->id(), /*fin=*/false, /*offset=*/0, stream_data));
+  // Datagram capsule.
+  std::string http_datagram_payload = {1, 2, 3, 4, 5, 6};
+  EXPECT_THAT(h3_datagram_visitor.received_h3_datagrams(),
+              ElementsAre(SavingHttp3DatagramVisitor::SavedHttp3Datagram{
+                  stream_->id(), http_datagram_payload}));
+  // Unknown capsule.
+  uint64_t capsule_type = 0x17u;
+  std::string unknown_capsule_payload = {1, 2, 3, 4};
+  EXPECT_THAT(h3_datagram_visitor.received_unknown_capsules(),
+              ElementsAre(SavingHttp3DatagramVisitor::SavedUnknownCapsule{
+                  stream_->id(), capsule_type, unknown_capsule_payload}));
+  // Cleanup.
+  stream_->UnregisterHttp3DatagramVisitor();
+}
+
+TEST_P(QuicSpdyClientStreamTest, CapsulesOnUnsuccessfulResponse) {
+  if (!VersionUsesHttp3(connection_->transport_version())) {
+    return;
+  }
+  SavingHttp3DatagramVisitor h3_datagram_visitor;
+  stream_->RegisterHttp3DatagramVisitor(&h3_datagram_visitor);
+  headers_[":status"] = "401";
+  headers_.erase("content-length");
+  auto headers = AsHeaderList(headers_);
+  stream_->OnStreamHeaderList(false, headers.uncompressed_header_bytes(),
+                              headers);
+  std::string capsule_data = {0, 6, 1, 2, 3, 4, 5, 6, 0x17, 4, 1, 2, 3, 4};
+  quiche::QuicheBuffer data_frame_header =
+      HttpEncoder::SerializeDataFrameHeader(
+          capsule_data.length(), quiche::SimpleBufferAllocator::Get());
+  std::string stream_data =
+      absl::StrCat(data_frame_header.AsStringView(), capsule_data);
+  stream_->OnStreamFrame(
+      QuicStreamFrame(stream_->id(), /*fin=*/false, /*offset=*/0, stream_data));
+  // Ensure received capsules were ignored.
+  EXPECT_TRUE(h3_datagram_visitor.received_h3_datagrams().empty());
+  EXPECT_TRUE(h3_datagram_visitor.received_unknown_capsules().empty());
+  // Cleanup.
+  stream_->UnregisterHttp3DatagramVisitor();
+}
+
 }  // 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 59405a6..1634202 100644
--- a/quiche/quic/core/http/quic_spdy_stream.cc
+++ b/quiche/quic/core/http/quic_spdy_stream.cc
@@ -1708,7 +1708,7 @@
 }
 
 void QuicSpdyStream::HandleBodyAvailable() {
-  if (!capsule_parser_) {
+  if (!capsule_parser_ || !uses_capsules()) {
     OnBodyAvailable();
     return;
   }
diff --git a/quiche/quic/core/http/quic_spdy_stream.h b/quiche/quic/core/http/quic_spdy_stream.h
index c0b7588..4de6fe2 100644
--- a/quiche/quic/core/http/quic_spdy_stream.h
+++ b/quiche/quic/core/http/quic_spdy_stream.h
@@ -373,6 +373,8 @@
 
   void set_headers_decompressed(bool val) { headers_decompressed_ = val; }
 
+  virtual bool uses_capsules() const { return capsule_parser_ != nullptr; }
+
   void set_ack_listener(
       quiche::QuicheReferenceCountedPointer<QuicAckListenerInterface>
           ack_listener) {