Skip to content

Commit cde9aeb

Browse files
authored
Fixes a bug in Envoy's HTTP/3-to-HTTP/1 proxying when a transfer-enco… (#39589)
Commit Message: QUIC Stream: Fixes a bug in Envoy's HTTP/3-to-HTTP/1 proxying when a "transfer-encoding" header is incorrectly appended. Additional Description: Envoy, when proxying via hyperloop to HTTP/1 backend, incorrectly interprets the end of an HTTP/3 HEADERS frame contained in a QUIC STREAM frame with the end_stream/fin bit set to true. This causes the HTTP/1 codec to mistakenly add a "transfer-encoding" header. This CL addresses this by inspecting the data before encoding headers. For requests containing only headers, it now explicitly signals the end of the stream with the "headers-only" indicator, which is the correct way to denote this in HTTP/3 (unlike the "fin" flag). This prevents the incorrect "transfer-encoding" header from being added. Risk Level: low, guarded by runtime guard Testing: internally tested with google3 e2e tests Docs Changes: Release Notes: Platform Specific Features: [Optional Runtime guard:] envoy_reloadable_features_quic_signal_headers_only_to_http1_backend [Optional Fixes #Issue] [Optional Fixes commit #PR or SHA] [Optional Deprecated:] [Optional [API Considerations](https://github.com/envoyproxy/envoy/blob/main/api/review_checklist.md):] --------- Signed-off-by: Ting Pan <panting@google.com>
1 parent 162ce8d commit cde9aeb

File tree

6 files changed

+43
-14
lines changed

6 files changed

+43
-14
lines changed

changelogs/current.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ bug_fixes:
7474
change: |
7575
Fixes a bug where the lifetime of the HttpConnectionManager's ActiveStream can be out of sync
7676
with the lifetime of the codec stream.
77+
- area: quic
78+
change: |
79+
Fixes a bug in Envoy's HTTP/3-to-HTTP/1 proxying when a "transfer-encoding" header is incorrectly appended.
80+
Protected by runtime guard ``envoy.reloadable_features.quic_signal_headers_only_to_http1_backend``.
7781
7882
removed_config_or_runtime:
7983
# *Normally occurs at the end of the* :ref:`deprecation period <deprecated>`

source/common/quic/envoy_quic_server_stream.cc

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,13 @@ void EnvoyQuicServerStream::OnInitialHeadersComplete(bool fin, size_t frame_len,
164164
}
165165

166166
ENVOY_STREAM_LOG(debug, "Received headers: {}.", *this, header_list.DebugString());
167-
if (fin) {
167+
const bool headers_only = fin_received() && highest_received_byte_offset() == NumBytesConsumed();
168+
const bool end_stream =
169+
fin ||
170+
(headers_only && Runtime::runtimeFeatureEnabled("envoy.reloadable_features.quic_signal_"
171+
"headers_only_to_http1_backend"));
172+
ENVOY_STREAM_LOG(debug, "Headers_only: {}, end_stream: {}.", *this, headers_only, end_stream);
173+
if (end_stream) {
168174
end_stream_decoded_ = true;
169175
}
170176
saw_regular_headers_ = false;
@@ -217,7 +223,7 @@ void EnvoyQuicServerStream::OnInitialHeadersComplete(bool fin, size_t frame_len,
217223

218224
Http::RequestDecoder* decoder = request_decoder_->get().ptr();
219225
if (decoder != nullptr) {
220-
decoder->decodeHeaders(std::move(headers), /*end_stream=*/fin);
226+
decoder->decodeHeaders(std::move(headers), /*end_stream=*/end_stream);
221227
}
222228
ConsumeHeaderList();
223229
}

source/common/runtime/runtime_features.cc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ RUNTIME_GUARD(envoy_reloadable_features_proxy_status_mapping_more_core_response_
7777
// Ignore the automated "remove this flag" issue: we should keep this for 1 year. Confirm with
7878
// @danzh2010 or @RyanTheOptimist before removing.
7979
RUNTIME_GUARD(envoy_reloadable_features_quic_send_server_preferred_address_to_all_clients);
80+
RUNTIME_GUARD(envoy_reloadable_features_quic_signal_headers_only_to_http1_backend);
8081
RUNTIME_GUARD(envoy_reloadable_features_quic_upstream_reads_fixed_number_packets);
8182
RUNTIME_GUARD(envoy_reloadable_features_quic_upstream_socket_use_address_cache_for_read);
8283
RUNTIME_GUARD(envoy_reloadable_features_report_load_with_rq_issued);

test/common/quic/envoy_quic_server_stream_test.cc

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ class EnvoyQuicServerStreamTest : public testing::Test {
263263
};
264264

265265
TEST_F(EnvoyQuicServerStreamTest, GetRequestAndResponse) {
266-
EXPECT_CALL(stream_decoder_, decodeHeaders_(_, /*end_stream=*/false))
266+
EXPECT_CALL(stream_decoder_, decodeHeaders_(_, /*end_stream=*/true))
267267
.WillOnce(Invoke([this](const Http::RequestHeaderMapSharedPtr& headers, bool) {
268268
EXPECT_EQ(host_, headers->getHostValue());
269269
EXPECT_EQ("/", headers->getPathValue());
@@ -273,7 +273,6 @@ TEST_F(EnvoyQuicServerStreamTest, GetRequestAndResponse) {
273273
EXPECT_EQ("a=b; c=d",
274274
headers->get(Http::Headers::get().Cookie)[0]->value().getStringView());
275275
}));
276-
EXPECT_CALL(stream_decoder_, decodeData(BufferStringEqual(""), /*end_stream=*/true));
277276
quiche::HttpHeaderBlock spdy_headers;
278277
spdy_headers[":authority"] = host_;
279278
spdy_headers[":method"] = "GET";

test/integration/multiplexed_integration_test.cc

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -811,11 +811,6 @@ TEST_P(MetadataIntegrationTest, ConsumeAndInsertRequestMetadata) {
811811
// Verifies a headers metadata added.
812812
std::set<std::string> expected_metadata_keys = {"headers"};
813813
expected_metadata_keys.insert("metadata");
814-
if (downstreamProtocol() == Http::CodecType::HTTP3) {
815-
// HTTP/3 Sends "end stream" in an empty DATA frame which results in the test filter
816-
// adding the "data" metadata header.
817-
expected_metadata_keys.insert("data");
818-
}
819814
verifyExpectedMetadata(upstream_request_->metadataMap(), expected_metadata_keys);
820815

821816
// Sends a headers only request with metadata. An empty data frame carries end_stream.
@@ -927,11 +922,6 @@ void MetadataIntegrationTest::verifyHeadersOnlyTest() {
927922
// Verifies a headers metadata added.
928923
std::set<std::string> expected_metadata_keys = {"headers"};
929924
expected_metadata_keys.insert("metadata");
930-
if (downstreamProtocol() == Http::CodecType::HTTP3) {
931-
// HTTP/3 Sends "end stream" in an empty DATA frame which results in the test filter
932-
// adding the "data" metadata header.
933-
expected_metadata_keys.insert("data");
934-
}
935925
verifyExpectedMetadata(upstream_request_->metadataMap(), expected_metadata_keys);
936926

937927
// Verifies zero length data received, and end_stream is true.

test/integration/protocol_integration_test.cc

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5742,4 +5742,33 @@ TEST_P(DownstreamProtocolIntegrationTest, DownstreamCxStats) {
57425742
test_server_->waitForCounterGe("http.config_test.downstream_cx_tx_bytes_total", 512);
57435743
}
57445744

5745+
// When upstream protocol is HTTP1, an OPTIONS request with no body will not
5746+
// append transfer-encoding chunked.
5747+
TEST_P(DownstreamProtocolIntegrationTest, OptionsWithNoBodyNotChunked) {
5748+
// This test is only relevant to H1 upstream.
5749+
if (upstreamProtocol() != Http::CodecType::HTTP1) {
5750+
return;
5751+
}
5752+
5753+
initialize();
5754+
codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http")));
5755+
5756+
Http::TestRequestHeaderMapImpl request_headers{
5757+
{":method", "OPTIONS"},
5758+
{":path", "/foo"},
5759+
{":scheme", "http"},
5760+
{":authority", "host"},
5761+
{"access-control-request-method", "GET"},
5762+
{"origin", "test-origin"},
5763+
};
5764+
auto response = codec_client_->makeHeaderOnlyRequest(request_headers);
5765+
waitForNextUpstreamRequest();
5766+
EXPECT_EQ(upstream_request_->headers().TransferEncoding(), nullptr);
5767+
upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, true);
5768+
ASSERT_TRUE(response->waitForEndStream());
5769+
EXPECT_TRUE(response->complete());
5770+
EXPECT_THAT(response->headers(), Http::HttpStatusIs("200"));
5771+
EXPECT_EQ(response->headers().TransferEncoding(), nullptr);
5772+
}
5773+
57455774
} // namespace Envoy

0 commit comments

Comments
 (0)