Support multiple interim responses in SimpleClient.

Currently, SimpleClient and descendants support 100 Continue responses (an
interim response) by reading in 100 Continue to response_headers(). Then once
100 Continue is populated and verified, the client calls
ClearPreliminaryResponse() so that response_headers() can be reused for the
final response.

This CL updates SimpleClient and descendants to support multiple interim
responses by introducing interim_headers(), an ordered sequence of BalsaHeaders
for interim responses. This CL removes ClearPreliminaryResponse() and adds
WaitForInterimResponse(), which in the future may be called multiple times.

SimpleHttpClient also calls BalsaFrame::set_use_interim_headers_callback(true),
which gives some additional testing confidence in this new code path.

This CL simplifies some 100-specific logic in the test clients. However, the
client continues to treat 101 as a final response (even in SimpleHttp2Client,
for consistency with SimpleHttpClient) due to the issue in cl/528583898.

One part of this CL involves updates to the QUIC (test) client ecosystem. I
decided to replace the interim/preliminary headers functionality in
QuicTestClient, as updating interim headers on querying (rather than on
receipt) can interact strangely with stream resets and timeouts. This CL
instead plumbs a callback to update interim headers with the flow
SimpleQuicClient --> GfeMockableQuicClient --> QuicSimpleClientSession -->
QuicSimpleClientStream, which can immediately update interim headers on
receipt. A comparison:

$ blaze test //gfe/gfe2/e2e:expect_100_continue_test_ietf_quic
Before: "PASSED in 307.2s" (http://sponge2/2c1eb63c-2bcc-4399-a329-98c638f562ba)
After: "PASSED in 11.0s" (http://sponge2/6c178f44-0aa3-4684-a4b0-e52fdbe61ae2)

I also verified with ++haoyuewang@ that the QuicSpdyClientStream changes do not
have a production impact on Hyperloop.

Huge inspiration from and thanks to ++bnc@'s foundational cl/379795244, which
has made this CL possible!

PiperOrigin-RevId: 529847861
diff --git a/quiche/quic/core/http/quic_spdy_client_stream.cc b/quiche/quic/core/http/quic_spdy_client_stream.cc
index 4eded5e..2f6eb8d 100644
--- a/quiche/quic/core/http/quic_spdy_client_stream.cc
+++ b/quiche/quic/core/http/quic_spdy_client_stream.cc
@@ -28,8 +28,7 @@
       response_code_(0),
       header_bytes_read_(0),
       header_bytes_written_(0),
-      session_(session),
-      has_preliminary_headers_(false) {}
+      session_(session) {}
 
 QuicSpdyClientStream::QuicSpdyClientStream(PendingStream* pending,
                                            QuicSpdyClientSession* session)
@@ -38,8 +37,7 @@
       response_code_(0),
       header_bytes_read_(0),
       header_bytes_written_(0),
-      session_(session),
-      has_preliminary_headers_(false) {}
+      session_(session) {}
 
 QuicSpdyClientStream::~QuicSpdyClientStream() = default;
 
@@ -74,13 +72,7 @@
                     << response_headers_[":status"].as_string() << " on stream "
                     << id();
     set_headers_decompressed(false);
-    if (response_code_ == 100 && !has_preliminary_headers_) {
-      // This is 100 Continue, save it to enable "Expect: 100-continue".
-      has_preliminary_headers_ = true;
-      preliminary_headers_ = std::move(response_headers_);
-    } else {
-      response_headers_.clear();
-    }
+    preliminary_headers_.push_back(std::move(response_headers_));
   }
 
   return true;
diff --git a/quiche/quic/core/http/quic_spdy_client_stream.h b/quiche/quic/core/http/quic_spdy_client_stream.h
index 9a952aa..02956c6 100644
--- a/quiche/quic/core/http/quic_spdy_client_stream.h
+++ b/quiche/quic/core/http/quic_spdy_client_stream.h
@@ -6,6 +6,7 @@
 #define QUICHE_QUIC_CORE_HTTP_QUIC_SPDY_CLIENT_STREAM_H_
 
 #include <cstddef>
+#include <list>
 #include <string>
 
 #include "absl/strings/string_view.h"
@@ -56,7 +57,7 @@
   // Returns whatever headers have been received for this stream.
   const spdy::Http2HeaderBlock& response_headers() { return response_headers_; }
 
-  const spdy::Http2HeaderBlock& preliminary_headers() {
+  const std::list<spdy::Http2HeaderBlock>& preliminary_headers() {
     return preliminary_headers_;
   }
 
@@ -96,11 +97,9 @@
 
   QuicSpdyClientSession* session_;
 
-  // These preliminary headers are used for the 100 Continue headers
-  // that may arrive before the response headers when the request has
-  // Expect: 100-continue.
-  bool has_preliminary_headers_;
-  spdy::Http2HeaderBlock preliminary_headers_;
+  // These preliminary headers are used for interim response headers that may
+  // arrive before the final response headers.
+  std::list<spdy::Http2HeaderBlock> preliminary_headers_;
 };
 
 }  // namespace quic
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 b249a98..2735b01 100644
--- a/quiche/quic/core/http/quic_spdy_client_stream_test.cc
+++ b/quiche/quic/core/http/quic_spdy_client_stream_test.cc
@@ -166,7 +166,9 @@
   auto headers = AsHeaderList(headers_);
   stream_->OnStreamHeaderList(false, headers.uncompressed_header_bytes(),
                               headers);
-  EXPECT_EQ("100", stream_->preliminary_headers().find(":status")->second);
+  ASSERT_EQ(stream_->preliminary_headers().size(), 1);
+  EXPECT_EQ("100",
+            stream_->preliminary_headers().front().find(":status")->second);
   EXPECT_EQ(0u, stream_->response_headers().size());
   EXPECT_EQ(100, stream_->response_code());
   EXPECT_EQ("", stream_->data());
@@ -187,7 +189,9 @@
   EXPECT_EQ(200, stream_->response_code());
   EXPECT_EQ(body_, stream_->data());
   // Make sure the 100 response is still available.
-  EXPECT_EQ("100", stream_->preliminary_headers().find(":status")->second);
+  ASSERT_EQ(stream_->preliminary_headers().size(), 1);
+  EXPECT_EQ("100",
+            stream_->preliminary_headers().front().find(":status")->second);
 }
 
 TEST_P(QuicSpdyClientStreamTest, TestUnknownInformationalBeforeSuccessful) {
@@ -196,6 +200,9 @@
   auto headers = AsHeaderList(headers_);
   stream_->OnStreamHeaderList(false, headers.uncompressed_header_bytes(),
                               headers);
+  ASSERT_EQ(stream_->preliminary_headers().size(), 1);
+  EXPECT_EQ("199",
+            stream_->preliminary_headers().front().find(":status")->second);
   EXPECT_EQ(0u, stream_->response_headers().size());
   EXPECT_EQ(199, stream_->response_code());
   EXPECT_EQ("", stream_->data());
@@ -215,6 +222,63 @@
   EXPECT_EQ("200", stream_->response_headers().find(":status")->second);
   EXPECT_EQ(200, stream_->response_code());
   EXPECT_EQ(body_, stream_->data());
+  // Make sure the 199 response is still available.
+  ASSERT_EQ(stream_->preliminary_headers().size(), 1);
+  EXPECT_EQ("199",
+            stream_->preliminary_headers().front().find(":status")->second);
+}
+
+TEST_P(QuicSpdyClientStreamTest, TestMultipleInformationalBeforeSuccessful) {
+  // First send 100 Continue.
+  headers_[":status"] = "100";
+  auto headers = AsHeaderList(headers_);
+  stream_->OnStreamHeaderList(false, headers.uncompressed_header_bytes(),
+                              headers);
+  ASSERT_EQ(stream_->preliminary_headers().size(), 1);
+  EXPECT_EQ("100",
+            stream_->preliminary_headers().front().find(":status")->second);
+  EXPECT_EQ(0u, stream_->response_headers().size());
+  EXPECT_EQ(100, stream_->response_code());
+  EXPECT_EQ("", stream_->data());
+
+  // Then send 199, an unknown Informational (1XX).
+  headers_[":status"] = "199";
+  headers = AsHeaderList(headers_);
+  stream_->OnStreamHeaderList(false, headers.uncompressed_header_bytes(),
+                              headers);
+  ASSERT_EQ(stream_->preliminary_headers().size(), 2);
+  EXPECT_EQ("100",
+            stream_->preliminary_headers().front().find(":status")->second);
+  EXPECT_EQ("199",
+            stream_->preliminary_headers().back().find(":status")->second);
+  EXPECT_EQ(0u, stream_->response_headers().size());
+  EXPECT_EQ(199, stream_->response_code());
+  EXPECT_EQ("", stream_->data());
+
+  // Then send 200 OK.
+  headers_[":status"] = "200";
+  headers = AsHeaderList(headers_);
+  stream_->OnStreamHeaderList(false, headers.uncompressed_header_bytes(),
+                              headers);
+  quiche::QuicheBuffer header = HttpEncoder::SerializeDataFrameHeader(
+      body_.length(), quiche::SimpleBufferAllocator::Get());
+  std::string data = VersionUsesHttp3(connection_->transport_version())
+                         ? absl::StrCat(header.AsStringView(), body_)
+                         : body_;
+  stream_->OnStreamFrame(
+      QuicStreamFrame(stream_->id(), /*fin=*/false, /*offset=*/0, data));
+
+  // Make sure the 200 response got parsed correctly.
+  EXPECT_EQ("200", stream_->response_headers().find(":status")->second);
+  EXPECT_EQ(200, stream_->response_code());
+  EXPECT_EQ(body_, stream_->data());
+
+  // Make sure the informational responses are still available.
+  ASSERT_EQ(stream_->preliminary_headers().size(), 2);
+  EXPECT_EQ("100",
+            stream_->preliminary_headers().front().find(":status")->second);
+  EXPECT_EQ("199",
+            stream_->preliminary_headers().back().find(":status")->second);
 }
 
 TEST_P(QuicSpdyClientStreamTest, TestReceiving101) {
diff --git a/quiche/quic/test_tools/quic_test_client.cc b/quiche/quic/test_tools/quic_test_client.cc
index 1f17227..5835800 100644
--- a/quiche/quic/test_tools/quic_test_client.cc
+++ b/quiche/quic/test_tools/quic_test_client.cc
@@ -616,7 +616,6 @@
   response_ = "";
   response_complete_ = false;
   response_headers_complete_ = false;
-  preliminary_headers_.clear();
   response_headers_.clear();
   response_trailers_.clear();
   bytes_read_ = 0;
@@ -668,18 +667,6 @@
   return &response_headers_;
 }
 
-const spdy::Http2HeaderBlock* QuicTestClient::preliminary_headers() const {
-  for (std::pair<QuicStreamId, QuicSpdyClientStream*> stream : open_streams_) {
-    size_t bytes_read =
-        stream.second->stream_bytes_read() + stream.second->header_bytes_read();
-    if (bytes_read > 0) {
-      preliminary_headers_ = stream.second->preliminary_headers().Clone();
-      break;
-    }
-  }
-  return &preliminary_headers_;
-}
-
 const spdy::Http2HeaderBlock& QuicTestClient::response_trailers() const {
   return response_trailers_;
 }
@@ -737,7 +724,6 @@
           client_stream->stream_error(), connected(),
           client_stream->headers_decompressed(),
           client_stream->response_headers(),
-          client_stream->preliminary_headers(),
           (buffer_body() ? std::string(client_stream->data()) : ""),
           client_stream->received_trailers(),
           // Use NumBytesConsumed to avoid counting retransmitted stream frames.
@@ -844,7 +830,6 @@
       response_complete(other.response_complete),
       response_headers_complete(other.response_headers_complete),
       response_headers(other.response_headers.Clone()),
-      preliminary_headers(other.preliminary_headers.Clone()),
       response(other.response),
       response_trailers(other.response_trailers.Clone()),
       bytes_read(other.bytes_read),
@@ -855,7 +840,6 @@
     QuicRstStreamErrorCode stream_error, bool response_complete,
     bool response_headers_complete,
     const spdy::Http2HeaderBlock& response_headers,
-    const spdy::Http2HeaderBlock& preliminary_headers,
     const std::string& response,
     const spdy::Http2HeaderBlock& response_trailers, uint64_t bytes_read,
     uint64_t bytes_written, int64_t response_body_size)
@@ -863,7 +847,6 @@
       response_complete(response_complete),
       response_headers_complete(response_headers_complete),
       response_headers(response_headers.Clone()),
-      preliminary_headers(preliminary_headers.Clone()),
       response(response),
       response_trailers(response_trailers.Clone()),
       bytes_read(bytes_read),
@@ -896,7 +879,6 @@
   response_ = state.response;
   response_complete_ = state.response_complete;
   response_headers_complete_ = state.response_headers_complete;
-  preliminary_headers_ = state.preliminary_headers.Clone();
   response_headers_ = state.response_headers.Clone();
   response_trailers_ = state.response_trailers.Clone();
   bytes_read_ = state.bytes_read;
diff --git a/quiche/quic/test_tools/quic_test_client.h b/quiche/quic/test_tools/quic_test_client.h
index 9f2a869..4b95046 100644
--- a/quiche/quic/test_tools/quic_test_client.h
+++ b/quiche/quic/test_tools/quic_test_client.h
@@ -176,7 +176,6 @@
   // have received a partial response.
   bool response_headers_complete() const;
   const spdy::Http2HeaderBlock* response_headers() const;
-  const spdy::Http2HeaderBlock* preliminary_headers() const;
   int64_t response_size() const;
   size_t bytes_read() const;
   size_t bytes_written() const;
@@ -358,7 +357,6 @@
     PerStreamState(QuicRstStreamErrorCode stream_error, bool response_complete,
                    bool response_headers_complete,
                    const spdy::Http2HeaderBlock& response_headers,
-                   const spdy::Http2HeaderBlock& preliminary_headers,
                    const std::string& response,
                    const spdy::Http2HeaderBlock& response_trailers,
                    uint64_t bytes_read, uint64_t bytes_written,
@@ -369,7 +367,6 @@
     bool response_complete;
     bool response_headers_complete;
     spdy::Http2HeaderBlock response_headers;
-    spdy::Http2HeaderBlock preliminary_headers;
     std::string response;
     spdy::Http2HeaderBlock response_trailers;
     uint64_t bytes_read;
@@ -402,7 +399,6 @@
 
   bool response_complete_;
   bool response_headers_complete_;
-  mutable spdy::Http2HeaderBlock preliminary_headers_;
   mutable spdy::Http2HeaderBlock response_headers_;
 
   // Parsed response trailers (if present), copied from the stream in OnClose.
diff --git a/quiche/quic/tools/quic_simple_client_session.cc b/quiche/quic/tools/quic_simple_client_session.cc
index eaaa921..0bb1515 100644
--- a/quiche/quic/tools/quic_simple_client_session.cc
+++ b/quiche/quic/tools/quic_simple_client_session.cc
@@ -36,9 +36,11 @@
 
 std::unique_ptr<QuicSpdyClientStream>
 QuicSimpleClientSession::CreateClientStream() {
-  return std::make_unique<QuicSimpleClientStream>(
+  auto stream = std::make_unique<QuicSimpleClientStream>(
       GetNextOutgoingBidirectionalStreamId(), this, BIDIRECTIONAL,
       drop_response_body_);
+  stream->set_on_interim_headers(on_interim_headers_);
+  return stream;
 }
 
 bool QuicSimpleClientSession::ShouldNegotiateWebTransport() {
diff --git a/quiche/quic/tools/quic_simple_client_session.h b/quiche/quic/tools/quic_simple_client_session.h
index b172afc..2a0db5d 100644
--- a/quiche/quic/tools/quic_simple_client_session.h
+++ b/quiche/quic/tools/quic_simple_client_session.h
@@ -5,9 +5,13 @@
 #ifndef QUICHE_QUIC_TOOLS_QUIC_SIMPLE_CLIENT_SESSION_H_
 #define QUICHE_QUIC_TOOLS_QUIC_SIMPLE_CLIENT_SESSION_H_
 
+#include <functional>
+#include <utility>
+
 #include "quiche/quic/core/http/quic_spdy_client_session.h"
 #include "quiche/quic/tools/quic_client_base.h"
 #include "quiche/quic/tools/quic_simple_client_stream.h"
+#include "quiche/spdy/core/http2_header_block.h"
 
 namespace quic {
 
@@ -42,7 +46,13 @@
       std::unique_ptr<QuicPathValidationContext> context) override;
   bool drop_response_body() const { return drop_response_body_; }
 
+  void set_on_interim_headers(
+      std::function<void(const spdy::Http2HeaderBlock&)> on_interim_headers) {
+    on_interim_headers_ = std::move(on_interim_headers);
+  }
+
  private:
+  std::function<void(const spdy::Http2HeaderBlock&)> on_interim_headers_;
   QuicClientBase::NetworkHelper* network_helper_;
   const bool drop_response_body_;
   const bool enable_web_transport_;
diff --git a/quiche/quic/tools/quic_simple_client_stream.cc b/quiche/quic/tools/quic_simple_client_stream.cc
index 14de9f0..76dd39f 100644
--- a/quiche/quic/tools/quic_simple_client_stream.cc
+++ b/quiche/quic/tools/quic_simple_client_stream.cc
@@ -4,6 +4,8 @@
 
 #include "quiche/quic/tools/quic_simple_client_stream.h"
 
+#include "quiche/common/platform/api/quiche_logging.h"
+
 namespace quic {
 
 void QuicSimpleClientStream::OnBodyAvailable() {
@@ -26,4 +28,20 @@
   }
 }
 
+bool QuicSimpleClientStream::ParseAndValidateStatusCode() {
+  const size_t num_previous_interim_headers = preliminary_headers().size();
+  if (!QuicSpdyClientStream::ParseAndValidateStatusCode()) {
+    return false;
+  }
+  // The base ParseAndValidateStatusCode() may have added a preliminary header.
+  if (preliminary_headers().size() > num_previous_interim_headers) {
+    QUICHE_DCHECK_EQ(preliminary_headers().size(),
+                     num_previous_interim_headers + 1);
+    if (on_interim_headers_) {
+      on_interim_headers_(preliminary_headers().back());
+    }
+  }
+  return true;
+}
+
 }  // namespace quic
diff --git a/quiche/quic/tools/quic_simple_client_stream.h b/quiche/quic/tools/quic_simple_client_stream.h
index 976ceaf..fd7ede4 100644
--- a/quiche/quic/tools/quic_simple_client_stream.h
+++ b/quiche/quic/tools/quic_simple_client_stream.h
@@ -5,6 +5,9 @@
 #ifndef QUICHE_QUIC_TOOLS_QUIC_SIMPLE_CLIENT_STREAM_H_
 #define QUICHE_QUIC_TOOLS_QUIC_SIMPLE_CLIENT_STREAM_H_
 
+#include <functional>
+#include <utility>
+
 #include "quiche/quic/core/http/quic_spdy_client_stream.h"
 
 namespace quic {
@@ -17,7 +20,16 @@
         drop_response_body_(drop_response_body) {}
   void OnBodyAvailable() override;
 
+  void set_on_interim_headers(
+      std::function<void(const spdy::Http2HeaderBlock&)> on_interim_headers) {
+    on_interim_headers_ = std::move(on_interim_headers);
+  }
+
+ protected:
+  bool ParseAndValidateStatusCode() override;
+
  private:
+  std::function<void(const spdy::Http2HeaderBlock&)> on_interim_headers_;
   const bool drop_response_body_;
 };
 
diff --git a/quiche/quic/tools/quic_spdy_client_base.cc b/quiche/quic/tools/quic_spdy_client_base.cc
index b46e780..248e379 100644
--- a/quiche/quic/tools/quic_spdy_client_base.cc
+++ b/quiche/quic/tools/quic_spdy_client_base.cc
@@ -7,6 +7,7 @@
 #include <utility>
 
 #include "absl/strings/numbers.h"
+#include "absl/strings/str_cat.h"
 #include "absl/strings/string_view.h"
 #include "quiche/quic/core/crypto/quic_random.h"
 #include "quiche/quic/core/http/spdy_utils.h"
@@ -86,8 +87,10 @@
       QUIC_LOG(ERROR) << "Invalid :status response header: " << status->second;
     }
     latest_response_headers_ = response_headers.DebugString();
-    preliminary_response_headers_ =
-        client_stream->preliminary_headers().DebugString();
+    for (const Http2HeaderBlock& headers :
+         client_stream->preliminary_headers()) {
+      absl::StrAppend(&preliminary_response_headers_, headers.DebugString());
+    }
     latest_response_header_block_ = response_headers.Clone();
     latest_response_body_ = std::string(client_stream->data());
     latest_response_trailers_ =