Implementing Indeterminate-Length Binary HTTP decoding

[RFC 9292](https://www.rfc-editor.org/rfc/rfc9292.html)

Protected by tests, this is new code not used by GFE.

PiperOrigin-RevId: 778634399
diff --git a/quiche/binary_http/binary_http_message.cc b/quiche/binary_http/binary_http_message.cc
index 815b30e..bc7a9b0 100644
--- a/quiche/binary_http/binary_http_message.cc
+++ b/quiche/binary_http/binary_http_message.cc
@@ -10,11 +10,13 @@
 #include <utility>
 #include <vector>
 
+#include "absl/base/attributes.h"
 #include "absl/container/flat_hash_map.h"
 #include "absl/status/status.h"
 #include "absl/status/statusor.h"
 #include "absl/strings/ascii.h"
 #include "absl/strings/str_cat.h"
+#include "absl/strings/str_format.h"
 #include "absl/strings/str_join.h"
 #include "absl/strings/string_view.h"
 #include "quiche/common/quiche_callbacks.h"
@@ -26,6 +28,15 @@
 
 constexpr uint64_t kKnownLengthRequestFraming = 0;
 constexpr uint64_t kKnownLengthResponseFraming = 1;
+constexpr uint64_t kIndeterminateLengthRequestFraming = 2;
+constexpr uint64_t kContentTerminator = 0;
+
+// A view of a field name and value. Used to pass around a field without owning
+// or copying the backing data.
+struct FieldView {
+  absl::string_view name;
+  absl::string_view value;
+};
 
 bool ReadStringValue(quiche::QuicheDataReader& reader, std::string& data) {
   absl::string_view data_view;
@@ -59,6 +70,21 @@
   return control_data;
 }
 
+// Decodes a header/trailer name and value. This takes a length which represents
+// only the name length.
+absl::StatusOr<FieldView> DecodeField(QuicheDataReader& reader,
+                                      uint64_t name_length) {
+  absl::string_view name;
+  if (!reader.ReadStringPiece(&name, name_length)) {
+    return absl::OutOfRangeError("Not enough data to read field name.");
+  }
+  absl::string_view value;
+  if (!reader.ReadStringPieceVarInt62(&value)) {
+    return absl::OutOfRangeError("Not enough data to read field value.");
+  }
+  return FieldView{name, value};
+}
+
 absl::Status DecodeFields(quiche::QuicheDataReader& reader,
                           quiche::UnretainedCallback<void(
                               absl::string_view name, absl::string_view value)>
@@ -402,6 +428,184 @@
       absl::StrCat("Unsupported framing type ", framing));
 }
 
+absl::Status
+BinaryHttpRequest::IndeterminateLengthDecoder::DecodeContentTerminatedSection(
+    QuicheDataReader& reader) {
+  uint64_t length_or_content_terminator;
+  do {
+    if (!reader.ReadVarInt62(&length_or_content_terminator)) {
+      return absl::OutOfRangeError("Not enough data to read section.");
+    }
+    if (length_or_content_terminator != kContentTerminator) {
+      switch (current_section_) {
+        case MessageSection::kHeader: {
+          const absl::StatusOr<FieldView> field =
+              DecodeField(reader, length_or_content_terminator);
+          if (!field.ok()) {
+            return field.status();
+          }
+          message_section_handler_.OnHeader(field->name, field->value);
+          break;
+        }
+        case MessageSection::kBody: {
+          absl::string_view body_chunk;
+          if (!reader.ReadStringPiece(&body_chunk,
+                                      length_or_content_terminator)) {
+            return absl::OutOfRangeError("Failed to read body chunk.");
+          }
+          message_section_handler_.OnBodyChunk(body_chunk);
+          break;
+        }
+        case MessageSection::kTrailer: {
+          const absl::StatusOr<FieldView> field =
+              DecodeField(reader, length_or_content_terminator);
+          if (!field.ok()) {
+            return field.status();
+          }
+          message_section_handler_.OnTrailer(field->name, field->value);
+          break;
+        }
+        default:
+          return absl::InternalError(
+              "Unexpected section in DecodeContentTerminatedSection.");
+      }
+    }
+    // Either a section was successfully decoded or a content terminator was
+    // encountered, save the checkpoint.
+    SaveCheckpoint(reader);
+  } while (length_or_content_terminator != kContentTerminator);
+  return absl::OkStatus();
+}
+
+// Returns Ok status only if the decoding processes the Padding section
+// successfully or if the message is truncated properly. All other points of
+// return are errors.
+absl::Status
+BinaryHttpRequest::IndeterminateLengthDecoder::DecodeCheckpointData(
+    bool end_stream) {
+  QuicheDataReader reader(checkpoint_view_);
+  switch (current_section_) {
+    case MessageSection::kEnd:
+      return absl::InternalError("Decoder is invalid.");
+    case MessageSection::kControlData: {
+      uint64_t framing;
+      if (!reader.ReadVarInt62(&framing)) {
+        return absl::OutOfRangeError("Failed to read framing.");
+      }
+      if (framing != kIndeterminateLengthRequestFraming) {
+        return absl::InvalidArgumentError(
+            absl::StrFormat("Unsupported framing type: 0x%02x", framing));
+      }
+
+      const absl::StatusOr<BinaryHttpRequest::ControlData> control_data =
+          DecodeControlData(reader);
+      // Only fails if there is not enough data to read the entire control data.
+      if (!control_data.ok()) {
+        return absl::OutOfRangeError("Failed to read control data.");
+      }
+
+      message_section_handler_.OnControlData(control_data.value());
+      SaveCheckpoint(reader);
+      current_section_ = MessageSection::kHeader;
+    }
+      ABSL_FALLTHROUGH_INTENDED;
+    case MessageSection::kHeader: {
+      const absl::Status status = DecodeContentTerminatedSection(reader);
+      if (!status.ok()) {
+        return status;
+      }
+      message_section_handler_.OnHeadersDone();
+      current_section_ = MessageSection::kBody;
+    }
+      ABSL_FALLTHROUGH_INTENDED;
+    case MessageSection::kBody: {
+      if (!reader.IsDoneReading()) {
+        maybe_truncated_ = false;
+      }
+      // Body and trailers truncation is valid only if:
+      // 1. There is no data to read after the headers section.
+      // 2. This is signaled as the last piece of data (end_stream).
+      if (maybe_truncated_ && end_stream) {
+        message_section_handler_.OnBodyChunksDone();
+        message_section_handler_.OnTrailersDone();
+        return absl::OkStatus();
+      }
+
+      const absl::Status status = DecodeContentTerminatedSection(reader);
+      if (!status.ok()) {
+        return status;
+      }
+      message_section_handler_.OnBodyChunksDone();
+      current_section_ = MessageSection::kTrailer;
+      // Reset the truncation flag before entering the trailers section.
+      maybe_truncated_ = true;
+    }
+      ABSL_FALLTHROUGH_INTENDED;
+    case MessageSection::kTrailer: {
+      if (!reader.IsDoneReading()) {
+        maybe_truncated_ = false;
+      }
+      // Trailers truncation is valid only if:
+      // 1. There is no data to read after the body section.
+      // 2. This is signaled as the last piece of data (end_stream).
+      if (maybe_truncated_ && end_stream) {
+        message_section_handler_.OnTrailersDone();
+        return absl::OkStatus();
+      }
+
+      const absl::Status status = DecodeContentTerminatedSection(reader);
+      if (!status.ok()) {
+        return status;
+      }
+      message_section_handler_.OnTrailersDone();
+      current_section_ = MessageSection::kPadding;
+    }
+      ABSL_FALLTHROUGH_INTENDED;
+    case MessageSection::kPadding: {
+      if (!IsValidPadding(reader.PeekRemainingPayload())) {
+        return absl::InvalidArgumentError("Non-zero padding.");
+      }
+      return absl::OkStatus();
+    }
+  }
+}
+
+void BinaryHttpRequest::IndeterminateLengthDecoder::InitializeCheckpoint(
+    absl::string_view data) {
+  checkpoint_view_ = data;
+  // Prepend buffered data if present. This is the data from a previous call to
+  // Decode that could not finish because it needed this new data.
+  if (!buffer_.empty()) {
+    absl::StrAppend(&buffer_, data);
+    checkpoint_view_ = buffer_;
+  }
+}
+
+absl::Status BinaryHttpRequest::IndeterminateLengthDecoder::Decode(
+    absl::string_view data, bool end_stream) {
+  if (current_section_ == MessageSection::kEnd) {
+    return absl::InternalError("Decoder is invalid.");
+  }
+
+  InitializeCheckpoint(data);
+  absl::Status status = DecodeCheckpointData(end_stream);
+  if (end_stream) {
+    current_section_ = MessageSection::kEnd;
+    buffer_.clear();
+    return status;
+  }
+  if (absl::IsOutOfRange(status)) {
+    BufferCheckpoint();
+    return absl::OkStatus();
+  }
+  if (!status.ok()) {
+    current_section_ = MessageSection::kEnd;
+  }
+
+  buffer_.clear();
+  return status;
+}
+
 absl::StatusOr<BinaryHttpResponse> BinaryHttpResponse::Create(
     absl::string_view data) {
   quiche::QuicheDataReader reader(data);
diff --git a/quiche/binary_http/binary_http_message.h b/quiche/binary_http/binary_http_message.h
index 8f1f8f8..85fb1dd 100644
--- a/quiche/binary_http/binary_http_message.h
+++ b/quiche/binary_http/binary_http_message.h
@@ -11,9 +11,11 @@
 #include <vector>
 
 #include "absl/container/flat_hash_map.h"
+#include "absl/status/status.h"
 #include "absl/status/statusor.h"
 #include "absl/strings/string_view.h"
 #include "quiche/common/platform/api/quiche_export.h"
+#include "quiche/common/quiche_data_reader.h"
 #include "quiche/common/quiche_data_writer.h"
 
 namespace quiche {
@@ -171,6 +173,91 @@
     return !(*this == rhs);
   }
 
+  // Provides a Decode method that can be called multiple times as data is
+  // received. The relevant MessageSectionHandler method will be called when
+  // its corresponding section is successfully decoded.
+  class IndeterminateLengthDecoder {
+   public:
+    // The handler to invoke when a section is decoded successfully.
+    class MessageSectionHandler {
+     public:
+      virtual ~MessageSectionHandler() = default;
+      virtual void OnControlData(const ControlData& control_data) = 0;
+      virtual void OnHeader(absl::string_view name,
+                            absl::string_view value) = 0;
+      virtual void OnHeadersDone() = 0;
+      virtual void OnBodyChunk(absl::string_view body_chunk) = 0;
+      virtual void OnBodyChunksDone() = 0;
+      virtual void OnTrailer(absl::string_view name,
+                             absl::string_view value) = 0;
+      virtual void OnTrailersDone() = 0;
+    };
+
+    explicit IndeterminateLengthDecoder(
+        MessageSectionHandler& message_section_handler)
+        : message_section_handler_(message_section_handler) {}
+
+    // Decodes an Indeterminate-Length BHTTP request. As the caller receives
+    // portions of the request, the caller can call this method with the request
+    // portion. The class keeps track of the current message section that is
+    // being decoded and buffers data if the section is not fully decoded so
+    // that the next call can continue decoding from where it left off. It will
+    // also invoke the appropriate MessageSectionHandler method when a section
+    // is decoded successfully.
+    // `end_stream` indicates that no more data will be provided to the decoder.
+    // This is used to determine if a valid message was decoded properly given
+    // the last piece of data provided, handling both complete messages and
+    // truncated messages.
+    absl::Status Decode(absl::string_view data, bool end_stream);
+
+   private:
+    // The sections of an Indeterminate-Length BHTTP request.
+    enum class MessageSection {
+      kControlData,
+      kHeader,
+      kBody,
+      kTrailer,
+      kPadding,
+      // The decoder is set to end after end_stream is received or when an error
+      // occurs while decoding.
+      kEnd,
+    };
+
+    // Initializes the checkpoint with the provided data and any buffered data.
+    void InitializeCheckpoint(absl::string_view data);
+    // Carries out the decode logic from the checkpoint. Returns
+    // OutOfRangeError if there is not enough data to decode the current
+    // section. When a section is fully decoded, the checkpoint is updated.
+    absl::Status DecodeCheckpointData(bool end_stream);
+    // Saves the checkpoint based on the current position of the reader.
+    void SaveCheckpoint(const QuicheDataReader& reader) {
+      checkpoint_view_ = reader.PeekRemainingPayload();
+    }
+    // Buffers the checkpoint.
+    void BufferCheckpoint() { buffer_ = std::string(checkpoint_view_); }
+    // Decodes a section 0 or more times until a content terminator is
+    // encountered.
+    absl::Status DecodeContentTerminatedSection(QuicheDataReader& reader);
+
+    MessageSectionHandler& message_section_handler_;
+    // Stores the data that could not be processed due to missing data.
+    std::string buffer_;
+    // Tracks the remaining data to be processed or buffered.
+    // When decoding fails due to missing data, we buffer based on this
+    // checkpoint and return. When decoding succeeds, we update the checkpoint
+    // to not buffer the already processed data.
+    absl::string_view checkpoint_view_;
+    // The current section that is being decoded.
+    MessageSection current_section_ = MessageSection::kControlData;
+    // Upon initial entry of the body or trailer section, the message is assumed
+    // to be truncated. This will be set to `false` upon the detection of data,
+    // and the state remains consistent for the remainder of the section. This
+    // serves to differentiate between true truncation and an `end_stream`
+    // occurring after partial processing of the section's content but before
+    // its content terminator.
+    bool maybe_truncated_ = true;
+  };
+
  private:
   absl::Status EncodeControlData(quiche::QuicheDataWriter& writer) const;
 
diff --git a/quiche/binary_http/binary_http_message_test.cc b/quiche/binary_http/binary_http_message_test.cc
index 0ae8e88..0a6e3d8 100644
--- a/quiche/binary_http/binary_http_message_test.cc
+++ b/quiche/binary_http/binary_http_message_test.cc
@@ -2,14 +2,19 @@
 
 #include <cstdint>
 #include <memory>
+#include <optional>
 #include <sstream>
 #include <string>
+#include <utility>
 #include <vector>
 
 #include "absl/container/flat_hash_map.h"
+#include "absl/status/status.h"
 #include "absl/strings/escaping.h"
+#include "absl/strings/str_cat.h"
 #include "absl/strings/string_view.h"
 #include "quiche/common/platform/api/quiche_test.h"
+#include "quiche/common/test_tools/quiche_test_utils.h"
 
 using ::testing::ContainerEq;
 using ::testing::FieldsAre;
@@ -30,6 +35,81 @@
   PrintTo(resp, &os);
   EXPECT_EQ(os.str(), resp.DebugString());
 }
+
+class RequestMessageSectionTestHandler
+    : public BinaryHttpRequest::IndeterminateLengthDecoder::
+          MessageSectionHandler {
+ public:
+  struct MessageData {
+    std::optional<BinaryHttpRequest::ControlData> control_data_;
+    std::vector<std::pair<std::string, std::string>> headers_;
+    bool headers_done_ = false;
+    std::vector<std::string> body_chunks_;
+    bool body_chunks_done_ = false;
+    std::vector<std::pair<std::string, std::string>> trailers_;
+    bool trailers_done_ = false;
+  };
+  RequestMessageSectionTestHandler() = default;
+  void OnControlData(
+      const BinaryHttpRequest::ControlData& control_data) override {
+    EXPECT_FALSE(message_data_.control_data_.has_value());
+    message_data_.control_data_ = control_data;
+  }
+  void OnHeader(absl::string_view name, absl::string_view value) override {
+    EXPECT_FALSE(message_data_.headers_done_);
+    message_data_.headers_.push_back({std::string(name), std::string(value)});
+  }
+  void OnHeadersDone() override {
+    EXPECT_FALSE(message_data_.headers_done_);
+    message_data_.headers_done_ = true;
+  }
+  void OnBodyChunk(absl::string_view body_chunk) override {
+    EXPECT_FALSE(message_data_.body_chunks_done_);
+    message_data_.body_chunks_.push_back(std::string(body_chunk));
+  }
+  void OnBodyChunksDone() override {
+    EXPECT_FALSE(message_data_.body_chunks_done_);
+    message_data_.body_chunks_done_ = true;
+  }
+  void OnTrailer(absl::string_view name, absl::string_view value) override {
+    EXPECT_FALSE(message_data_.trailers_done_);
+    message_data_.trailers_.push_back({std::string(name), std::string(value)});
+  }
+  void OnTrailersDone() override {
+    EXPECT_FALSE(message_data_.trailers_done_);
+    message_data_.trailers_done_ = true;
+  }
+  MessageData& GetMessageData() { return message_data_; }
+
+ private:
+  MessageData message_data_;
+};
+
+constexpr absl::string_view kIndeterminateLengthEncodedRequestHeaders =
+    "4002"                    // 2-byte framing indicator
+    "04504F5354"              // :method = POST
+    "056874747073"            // :scheme = https
+    "0A676F6F676C652E636F6D"  // :authority = "google.com"
+    "062F68656C6C6F"          // :path = /hello
+    "0A757365722D6167656E74"  // user-agent
+    "346375726C2F372E31362E33206C69626375726C2F372E31362E33204F70656E53534C2F"
+    "302E392E376C207A6C69622F312E322E33"  // curl/7.16.3 libcurl/7.16.3
+                                          // OpenSSL/0.9.7l zlib/1.2.3
+    "0F6163636570742D6C616E6775616765"    // accept-language
+    "06656E2C206D69"                      // en, mi
+    "C000000000000000";                   // 8-byte content terminator
+constexpr absl::string_view kIndeterminateLengthEncodedRequestBodyChunks =
+    "066368756E6B31"  // chunk1
+    "066368756E6B32"  // chunk2
+    "066368756E6B33"  // chunk3
+    "80000000";       // 4-byte content terminator
+constexpr absl::string_view kIndeterminateLengthEncodedRequestTrailers =
+    "08747261696C657231"  // trailer1
+    "0676616C756531"      // value1
+    "08747261696C657232"  // trailer2
+    "0676616C756532"      // value2
+    "00"                  // 1-byte content terminator
+    "000000";             // padding
 }  // namespace
 // Test examples from
 // https://www.ietf.org/archive/id/draft-ietf-httpbis-binary-message-06.html
@@ -388,6 +468,219 @@
   EXPECT_NE(request, no_body);
 }
 
+void ExpectRequestMessageSectionHandler(
+    const RequestMessageSectionTestHandler::MessageData& message_data) {
+  EXPECT_TRUE(message_data.control_data_.has_value());
+  if (message_data.control_data_.has_value()) {
+    EXPECT_THAT(*message_data.control_data_,
+                FieldsAre("POST", "https", "google.com", "/hello"));
+  }
+  std::vector<std::pair<std::string, std::string>> expected_headers = {
+      {"user-agent", "curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3"},
+      {"accept-language", "en, mi"}};
+  EXPECT_TRUE(message_data.headers_done_);
+  EXPECT_THAT(message_data.headers_, ContainerEq(expected_headers));
+  std::vector<std::string> expected_body_chunks = {"chunk1", "chunk2",
+                                                   "chunk3"};
+  EXPECT_TRUE(message_data.body_chunks_done_);
+  EXPECT_THAT(message_data.body_chunks_, ContainerEq(expected_body_chunks));
+  std::vector<std::pair<std::string, std::string>> expected_trailers = {
+      {"trailer1", "value1"}, {"trailer2", "value2"}};
+  EXPECT_TRUE(message_data.trailers_done_);
+  EXPECT_THAT(message_data.trailers_, ContainerEq(expected_trailers));
+}
+
+TEST(IndeterminateLengthDecoder, FullRequestDecodingSuccess) {
+  std::string request_bytes;
+  EXPECT_TRUE(absl::HexStringToBytes(
+      absl::StrCat(kIndeterminateLengthEncodedRequestHeaders,
+                   kIndeterminateLengthEncodedRequestBodyChunks,
+                   kIndeterminateLengthEncodedRequestTrailers),
+      &request_bytes));
+  RequestMessageSectionTestHandler handler;
+  BinaryHttpRequest::IndeterminateLengthDecoder decoder(handler);
+  QUICHE_EXPECT_OK(decoder.Decode(request_bytes, true));
+  ExpectRequestMessageSectionHandler(handler.GetMessageData());
+}
+
+TEST(IndeterminateLengthDecoder, BufferedRequestDecodingSuccess) {
+  std::string request_bytes;
+  EXPECT_TRUE(absl::HexStringToBytes(
+      absl::StrCat(kIndeterminateLengthEncodedRequestHeaders,
+                   kIndeterminateLengthEncodedRequestBodyChunks,
+                   kIndeterminateLengthEncodedRequestTrailers),
+      &request_bytes));
+  RequestMessageSectionTestHandler handler;
+  BinaryHttpRequest::IndeterminateLengthDecoder decoder(handler);
+  for (uint64_t i = 0; i < request_bytes.size() - 1; i++) {
+    QUICHE_EXPECT_OK(
+        decoder.Decode(absl::string_view(&request_bytes[i], 1), false));
+  }
+  // Decode the last byte, send end_stream.
+  QUICHE_EXPECT_OK(decoder.Decode(
+      absl::string_view(&request_bytes[request_bytes.size() - 1], 1), true));
+  ExpectRequestMessageSectionHandler(handler.GetMessageData());
+}
+
+TEST(IndeterminateLengthDecoder, InvalidFramingError) {
+  RequestMessageSectionTestHandler handler;
+  BinaryHttpRequest::IndeterminateLengthDecoder decoder(handler);
+  std::string request_bytes;
+  EXPECT_TRUE(absl::HexStringToBytes("00", &request_bytes));
+  absl::Status status = decoder.Decode(request_bytes, false);
+  EXPECT_THAT(status, test::StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(IndeterminateLengthDecoder, InvalidPaddingError) {
+  RequestMessageSectionTestHandler handler;
+  BinaryHttpRequest::IndeterminateLengthDecoder decoder(handler);
+  std::string request_bytes;
+  EXPECT_TRUE(absl::HexStringToBytes(
+      absl::StrCat(absl::StrCat(kIndeterminateLengthEncodedRequestHeaders,
+                                kIndeterminateLengthEncodedRequestBodyChunks,
+                                kIndeterminateLengthEncodedRequestTrailers)),
+      &request_bytes));
+  QUICHE_EXPECT_OK(decoder.Decode(request_bytes, false));
+  absl::Status status = decoder.Decode("\x01", false);
+  EXPECT_THAT(status, test::StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+void ExpectTruncatedTrailerSection(
+    const RequestMessageSectionTestHandler::MessageData& message_data) {
+  EXPECT_TRUE(message_data.headers_done_);
+  EXPECT_TRUE(message_data.trailers_done_);
+  std::vector<std::pair<std::string, std::string>> expected_headers = {
+      {"user-agent", "curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3"},
+      {"accept-language", "en, mi"}};
+  EXPECT_THAT(message_data.headers_, ContainerEq(expected_headers));
+  EXPECT_THAT(message_data.trailers_, testing::IsEmpty());
+}
+
+TEST(IndeterminateLengthDecoder, TruncatedBodyAndTrailers) {
+  RequestMessageSectionTestHandler handler;
+  BinaryHttpRequest::IndeterminateLengthDecoder decoder(handler);
+  std::string request_bytes;
+  EXPECT_TRUE(absl::HexStringToBytes(kIndeterminateLengthEncodedRequestHeaders,
+                                     &request_bytes));
+
+  QUICHE_EXPECT_OK(decoder.Decode(request_bytes, true));
+  auto message_data = handler.GetMessageData();
+  EXPECT_TRUE(message_data.body_chunks_done_);
+  EXPECT_THAT(message_data.body_chunks_, testing::IsEmpty());
+  ExpectTruncatedTrailerSection(message_data);
+}
+
+TEST(IndeterminateLengthDecoder, TruncatedBodyAndTrailersSplitEndStream) {
+  RequestMessageSectionTestHandler handler;
+  BinaryHttpRequest::IndeterminateLengthDecoder decoder(handler);
+  std::string request_bytes;
+  EXPECT_TRUE(absl::HexStringToBytes(kIndeterminateLengthEncodedRequestHeaders,
+                                     &request_bytes));
+
+  QUICHE_EXPECT_OK(decoder.Decode(request_bytes, false));
+  // Send `end_stream` with no data.
+  QUICHE_EXPECT_OK(decoder.Decode("", true));
+  auto message_data = handler.GetMessageData();
+  EXPECT_TRUE(message_data.body_chunks_done_);
+  EXPECT_THAT(message_data.body_chunks_, testing::IsEmpty());
+  ExpectTruncatedTrailerSection(message_data);
+}
+
+TEST(IndeterminateLengthDecoder, TruncatedTrailers) {
+  RequestMessageSectionTestHandler handler;
+  BinaryHttpRequest::IndeterminateLengthDecoder decoder(handler);
+  std::string request_bytes;
+  EXPECT_TRUE(absl::HexStringToBytes(
+      absl::StrCat(kIndeterminateLengthEncodedRequestHeaders,
+                   kIndeterminateLengthEncodedRequestBodyChunks),
+      &request_bytes));
+
+  QUICHE_EXPECT_OK(decoder.Decode(request_bytes, true));
+  auto message_data = handler.GetMessageData();
+  EXPECT_TRUE(message_data.body_chunks_done_);
+  std::vector<std::string> expected_body_chunks = {"chunk1", "chunk2",
+                                                   "chunk3"};
+  EXPECT_THAT(message_data.body_chunks_, ContainerEq(expected_body_chunks));
+  ExpectTruncatedTrailerSection(message_data);
+}
+
+TEST(IndeterminateLengthDecoder, TruncatedTrailersSplitEndStream) {
+  RequestMessageSectionTestHandler handler;
+  BinaryHttpRequest::IndeterminateLengthDecoder decoder(handler);
+  std::string request_bytes;
+  EXPECT_TRUE(absl::HexStringToBytes(
+      absl::StrCat(kIndeterminateLengthEncodedRequestHeaders,
+                   kIndeterminateLengthEncodedRequestBodyChunks),
+      &request_bytes));
+
+  QUICHE_EXPECT_OK(decoder.Decode(request_bytes, false));
+  // Send `end_stream` with no data.
+  QUICHE_EXPECT_OK(decoder.Decode("", true));
+  auto message_data = handler.GetMessageData();
+  EXPECT_TRUE(message_data.body_chunks_done_);
+  std::vector<std::string> expected_body_chunks = {"chunk1", "chunk2",
+                                                   "chunk3"};
+  EXPECT_THAT(message_data.body_chunks_, ContainerEq(expected_body_chunks));
+  ExpectTruncatedTrailerSection(message_data);
+}
+
+TEST(IndeterminateLengthDecoder, InvalidDecodeAfterEndStream) {
+  std::string request_bytes;
+  EXPECT_TRUE(absl::HexStringToBytes(kIndeterminateLengthEncodedRequestHeaders,
+                                     &request_bytes));
+  RequestMessageSectionTestHandler handler;
+  BinaryHttpRequest::IndeterminateLengthDecoder decoder(handler);
+  QUICHE_EXPECT_OK(decoder.Decode(request_bytes, true));
+  absl::Status status = decoder.Decode(request_bytes, false);
+  EXPECT_THAT(status, test::StatusIs(absl::StatusCode::kInternal));
+}
+
+struct InvalidEndStreamTestCase {
+  std::string name;
+  std::string request;
+};
+
+using InvalidEndStreamTest =
+    quiche::test::QuicheTestWithParam<InvalidEndStreamTestCase>;
+
+TEST_P(InvalidEndStreamTest, InvalidEndStreamError) {
+  const InvalidEndStreamTestCase& test_case = GetParam();
+  RequestMessageSectionTestHandler handler;
+  BinaryHttpRequest::IndeterminateLengthDecoder decoder(handler);
+  std::string request_bytes;
+  EXPECT_TRUE(absl::HexStringToBytes(test_case.request, &request_bytes));
+  absl::Status status = decoder.Decode(request_bytes, true);
+  EXPECT_THAT(status, test::StatusIs(absl::StatusCode::kOutOfRange));
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    InvalidEndStreamTestInstantiation, InvalidEndStreamTest,
+    testing::ValuesIn<InvalidEndStreamTestCase>({
+        {
+            "headers_not_terminated",
+            "02"                      // Indeterminate length request frame
+            "04504F5354"              // :method = POST
+            "056874747073"            // :scheme = https
+            "0A676F6F676C652E636F6D"  // :authority = "google.com"
+            "062F68656C6C6F"          // :path = /hello
+            "0A757365722D6167656E74"  // user-agent
+        },
+        {"body_not_terminated",
+         absl::StrCat(kIndeterminateLengthEncodedRequestHeaders,
+                      "066368756E6B31"  // chunk1
+                      )},
+        {"trailers_not_terminated",
+         absl::StrCat(kIndeterminateLengthEncodedRequestHeaders,
+                      kIndeterminateLengthEncodedRequestBodyChunks,
+                      "08747261696C657231"  // trailer1
+                      "0676616C756531"      // value1
+                      "08747261696C657232"  // trailer2
+                      )},
+    }),
+    [](const testing::TestParamInfo<InvalidEndStreamTest::ParamType>& info) {
+      return info.param.name;
+    });
+
 TEST(BinaryHttpResponse, EncodeNoBody) {
   /*
     HTTP/1.1 404 Not Found