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