Always reset content length status when removing the Content-Length header in BalsaHeaders.
Currently, when removing the "Content-Length" header, BalsaHeaders does not reset its internal content length status if "Transfer-Encoding: chunked" is also present. This can lead to inconsistencies where `content_length_valid()` remains true even after the "Content-Length" header is gone. This change introduces a new reloadable flag, `gfe2_reloadable_flag_reset_content_length_status_when_removing_content_length_header`, which, when true, ensures that the content length status is always reset when "Content-Length" is removed, regardless of the presence of "Transfer-Encoding: chunked". Tests are added to cover a scenario where both headers are initially present in a request.
I manually e2e tested, and confirmed that
* When gfe2_reloadable_flag_reset_content_length_status_when_removing_content_length_header is false, we saw `REQUIRED_BODY_BUT_NO_CONTENT_LENGTH` in the test backend http://screen/C7QyLfJxaEwCtsA.
* When the feature flag is true, the response code is 200, and the request headers sent by GFE has `transfer-encoding`: http://screen/9yDWwpGSFhnYVZi
I am now writing the nice automated e2e test that requires changes in the implementation. Given the tight timeline of the product launch, I don't think the automated test is blocking this change.
Protected by gfe2_reloadable_flag_reset_content_length_status_when_removing_content_length_header.
PiperOrigin-RevId: 836699619
diff --git a/quiche/balsa/balsa_headers.cc b/quiche/balsa/balsa_headers.cc
index ae4ce40..dcb3aa2 100644
--- a/quiche/balsa/balsa_headers.cc
+++ b/quiche/balsa/balsa_headers.cc
@@ -22,8 +22,10 @@
#include "absl/strings/string_view.h"
#include "quiche/balsa/balsa_enums.h"
#include "quiche/balsa/header_properties.h"
+#include "quiche/common/platform/api/quiche_flags.h"
#include "quiche/common/platform/api/quiche_header_policy.h"
#include "quiche/common/platform/api/quiche_logging.h"
+#include "quiche/common/quiche_feature_flags_list.h"
namespace {
@@ -323,7 +325,9 @@
// header we're removing is one of those headers.
void BalsaHeaders::MaybeClearSpecialHeaderValues(absl::string_view key) {
if (absl::EqualsIgnoreCase(key, kContentLength)) {
- if (transfer_encoding_is_chunked_) {
+ if (transfer_encoding_is_chunked_ &&
+ !GetQuicheReloadableFlag(
+ reset_content_length_status_when_removing_content_length_header)) {
return;
}
diff --git a/quiche/balsa/balsa_headers_test.cc b/quiche/balsa/balsa_headers_test.cc
index b00ca2b..f67a078 100644
--- a/quiche/balsa/balsa_headers_test.cc
+++ b/quiche/balsa/balsa_headers_test.cc
@@ -27,6 +27,7 @@
#include "quiche/balsa/http_validation_policy.h"
#include "quiche/balsa/simple_buffer.h"
#include "quiche/common/platform/api/quiche_expect_bug.h"
+#include "quiche/common/platform/api/quiche_flags.h"
#include "quiche/common/platform/api/quiche_logging.h"
#include "quiche/common/platform/api/quiche_test.h"
@@ -47,6 +48,11 @@
size_t size) {
headers->WriteFromFramer(ptr, size);
}
+
+ static void set_transfer_encoding_is_chunked(BalsaHeaders* headers,
+ bool value) {
+ headers->transfer_encoding_is_chunked_ = value;
+ }
};
namespace {
@@ -3164,7 +3170,24 @@
EXPECT_FALSE(headers.transfer_encoding_is_chunked());
}
-TEST(BalsaHeaders, ClearContentLength) {
+class BalsaHeadersClearContentLengthTest : public QuicheTestWithParam<bool> {
+ public:
+ BalsaHeadersClearContentLengthTest()
+ : flag_reset_content_length_status_when_removing_content_length_header_(
+ GetParam()) {
+ SetQuicheReloadableFlag(
+ reset_content_length_status_when_removing_content_length_header,
+ flag_reset_content_length_status_when_removing_content_length_header_);
+ }
+
+ protected:
+ bool flag_reset_content_length_status_when_removing_content_length_header_;
+};
+
+INSTANTIATE_TEST_SUITE_P(BalsaHeadersClearContentLengthTest,
+ BalsaHeadersClearContentLengthTest, testing::Bool());
+
+TEST_P(BalsaHeadersClearContentLengthTest, ClearContentLength) {
// Test that ClearContentLength() removes the content-length header and
// resets content_length_status().
BalsaHeaders headers;
@@ -3198,9 +3221,34 @@
EXPECT_EQ(BalsaHeadersEnums::NO_CONTENT_LENGTH,
headers.content_length_status());
EXPECT_FALSE(headers.content_length_valid());
+
+ // Populate the content-length header and set chunked encoding, to simulate
+ // the case where both content-length and chunked encoding are set in the
+ // header. Then test that ClearContentLength() removes the content-length
+ // header and resets content_length_status().
+ headers.SetContentLength(10);
+ // We cannot use SetTransferEncodingToChunkedAndClearContentLength() here
+ // because it will clear the content-length header internally.
+ headers.ReplaceOrAppendHeader("Transfer-Encoding", "chunked");
+ BalsaHeadersTestPeer::set_transfer_encoding_is_chunked(&headers, true);
+ headers.ClearContentLength();
+ EXPECT_EQ("chunked", headers.GetAllOfHeaderAsString("Transfer-Encoding"));
+ EXPECT_TRUE(headers.transfer_encoding_is_chunked());
+ EXPECT_FALSE(headers.HasHeader("Content-length"));
+ if (flag_reset_content_length_status_when_removing_content_length_header_) {
+ EXPECT_EQ(BalsaHeadersEnums::NO_CONTENT_LENGTH,
+ headers.content_length_status());
+ EXPECT_FALSE(headers.content_length_valid());
+ EXPECT_EQ(0u, headers.content_length());
+ } else {
+ EXPECT_EQ(BalsaHeadersEnums::VALID_CONTENT_LENGTH,
+ headers.content_length_status());
+ EXPECT_TRUE(headers.content_length_valid());
+ EXPECT_EQ(10u, headers.content_length());
+ }
}
-TEST(BalsaHeaders, ClearContentLengthByRemoveHeader) {
+TEST_P(BalsaHeadersClearContentLengthTest, ClearContentLengthByRemoveHeader) {
// Test that calling Remove() methods to clear the content-length header
// correctly resets internal content length fields.
BalsaHeaders headers;
@@ -3228,8 +3276,67 @@
EXPECT_FALSE(headers.content_length_valid());
}
+// Test that calling Remove() methods to clear the content-length header
+// correctly resets internal content length fields when the Transfer-Encoding
+// header is present.
+TEST_P(BalsaHeadersClearContentLengthTest,
+ ClearContentLengthByRemoveHeaderWithTransferEncodingHeader) {
+ BalsaHeaders headers;
+ headers.SetContentLength(10);
+ // We cannot use SetTransferEncodingToChunkedAndClearContentLength() here
+ // because it will clear the content-length header internally.
+ headers.ReplaceOrAppendHeader("Transfer-Encoding", "chunked");
+ BalsaHeadersTestPeer::set_transfer_encoding_is_chunked(&headers, true);
+ headers.RemoveAllOfHeader("Content-Length");
+ if (flag_reset_content_length_status_when_removing_content_length_header_) {
+ EXPECT_EQ(BalsaHeadersEnums::NO_CONTENT_LENGTH,
+ headers.content_length_status());
+ EXPECT_EQ(0u, headers.content_length());
+ EXPECT_FALSE(headers.content_length_valid());
+ } else {
+ EXPECT_EQ(BalsaHeadersEnums::VALID_CONTENT_LENGTH,
+ headers.content_length_status());
+ EXPECT_EQ(10u, headers.content_length());
+ EXPECT_TRUE(headers.content_length_valid());
+ }
+
+ headers.SetContentLength(11);
+ headers.ReplaceOrAppendHeader("Transfer-Encoding", "chunked");
+ BalsaHeadersTestPeer::set_transfer_encoding_is_chunked(&headers, true);
+ std::vector<absl::string_view> headers_to_remove;
+ headers_to_remove.emplace_back("Content-Length");
+ headers.RemoveAllOfHeaderInList(headers_to_remove);
+ if (flag_reset_content_length_status_when_removing_content_length_header_) {
+ EXPECT_EQ(BalsaHeadersEnums::NO_CONTENT_LENGTH,
+ headers.content_length_status());
+ EXPECT_EQ(0u, headers.content_length());
+ EXPECT_FALSE(headers.content_length_valid());
+ } else {
+ EXPECT_EQ(BalsaHeadersEnums::VALID_CONTENT_LENGTH,
+ headers.content_length_status());
+ EXPECT_EQ(11u, headers.content_length());
+ EXPECT_TRUE(headers.content_length_valid());
+ }
+
+ headers.SetContentLength(12);
+ headers.ReplaceOrAppendHeader("Transfer-Encoding", "chunked");
+ BalsaHeadersTestPeer::set_transfer_encoding_is_chunked(&headers, true);
+ headers.RemoveAllHeadersWithPrefix("Content");
+ if (flag_reset_content_length_status_when_removing_content_length_header_) {
+ EXPECT_EQ(BalsaHeadersEnums::NO_CONTENT_LENGTH,
+ headers.content_length_status());
+ EXPECT_EQ(0u, headers.content_length());
+ EXPECT_FALSE(headers.content_length_valid());
+ } else {
+ EXPECT_EQ(BalsaHeadersEnums::VALID_CONTENT_LENGTH,
+ headers.content_length_status());
+ EXPECT_EQ(12u, headers.content_length());
+ EXPECT_TRUE(headers.content_length_valid());
+ }
+}
+
// Chunk-encoding an identity-coded BalsaHeaders removes the identity-coding.
-TEST(BalsaHeaders, IdentityCodingToChunked) {
+TEST_P(BalsaHeadersClearContentLengthTest, IdentityCodingToChunked) {
std::string message =
"HTTP/1.1 200 OK\r\n"
"Transfer-Encoding: identity\r\n\r\n";
@@ -3253,7 +3360,7 @@
ElementsAre("chunked"));
}
-TEST(BalsaHeaders, SwitchContentLengthToChunk) {
+TEST_P(BalsaHeadersClearContentLengthTest, SwitchContentLengthToChunk) {
// Test that a header originally with content length header is correctly
// switched to using chunk encoding.
BalsaHeaders headers;
@@ -3272,7 +3379,7 @@
EXPECT_FALSE(headers.content_length_valid());
}
-TEST(BalsaHeaders, SwitchChunkedToContentLength) {
+TEST_P(BalsaHeadersClearContentLengthTest, SwitchChunkedToContentLength) {
// Test that a header originally with chunk encoding is correctly
// switched to using content length.
BalsaHeaders headers;
diff --git a/quiche/common/quiche_feature_flags_list.h b/quiche/common/quiche_feature_flags_list.h
index aec984a..cabd955 100755
--- a/quiche/common/quiche_feature_flags_list.h
+++ b/quiche/common/quiche_feature_flags_list.h
@@ -61,6 +61,7 @@
QUICHE_FLAG(bool, quiche_reloadable_flag_quic_testonly_default_true, true, true, "A testonly reloadable flag that will always default to true.")
QUICHE_FLAG(bool, quiche_reloadable_flag_quic_use_proof_source_get_cert_chains, false, true, "When true, quic::TlsServerHandshaker will use ProofSource::GetCertChains() instead of ProofSource::GetCertChain()")
QUICHE_FLAG(bool, quiche_reloadable_flag_quic_use_received_client_addresses_cache, true, true, "If true, use a LRU cache to record client addresses of packets received on server's original address.")
+QUICHE_FLAG(bool, quiche_reloadable_flag_reset_content_length_status_when_removing_content_length_header, false, false, "If true, the content_length_status field in balsa_headers will always be reset when the Content-Length header is removed, no matter if the Transfer-Encoding header is chuncked or not.")
QUICHE_FLAG(bool, quiche_restart_flag_quic_dispatcher_close_connection_on_invalid_ack, false, false, "An invalid ack is an ack that the peer sent for a packet that was not sent by the dispatcher. If true, the dispatcher will close the connection if it receives an invalid ack.")
QUICHE_FLAG(bool, quiche_restart_flag_quic_shed_tls_handshake_config, false, false, "If true, QUIC connections will call SSL_set_shed_handshake_config to drop BoringSSL handshake state after the handshake finishes in order to save memory.")
QUICHE_FLAG(bool, quiche_restart_flag_quic_stop_parsing_legacy_version_info, false, false, "If true, disable parsing the legacy version information transport parameter.")