Skip to content

Commit

Permalink
[balsa] fix for 1xx response mixup
Browse files Browse the repository at this point in the history
Signed-off-by: Paul Ogilby <[email protected]>

Signed-off-by: Ryan Northey <[email protected]>
  • Loading branch information
paul-r-gall authored and phlax committed Dec 18, 2024
1 parent 306b517 commit c908963
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 1 deletion.
4 changes: 4 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ bug_fixes:
- area: happy_eyeballs
change: |
Validate that ``additional_address`` are IP addresses instead of crashing when sorting.
- area: balsa
change: |
Fix incorrect handling of non-101 1xx responses. This fix can be temporarily reverted by setting runtime guard
``envoy.reloadable_features.wait_for_first_byte_before_balsa_msg_done`` to false.
removed_config_or_runtime:
# *Normally occurs at the end of the* :ref:`deprecation period <deprecated>`
Expand Down
5 changes: 4 additions & 1 deletion source/common/http/http1/balsa_parser.cc
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,10 @@ void BalsaParser::HeaderDone() {
void BalsaParser::ContinueHeaderDone() {}

void BalsaParser::MessageDone() {
if (status_ == ParserStatus::Error) {
if (status_ == ParserStatus::Error ||
// In the case of early 1xx, MessageDone() can be called twice in a row.
// The !first_byte_processed_ check is to make this function idempotent.
(wait_for_first_byte_before_msg_done_ && !first_byte_processed_)) {
return;
}
status_ = convertResult(connection_->onMessageComplete());
Expand Down
3 changes: 3 additions & 0 deletions source/common/http/http1/balsa_parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ class BalsaParser : public Parser, public quiche::BalsaVisitorInterface {
// Latched value of `envoy.reloadable_features.http1_balsa_delay_reset`.
const bool delay_reset_ =
Runtime::runtimeFeatureEnabled("envoy.reloadable_features.http1_balsa_delay_reset");
// Latched value of `envoy.reloadable_features.wait_for_first_byte_before_balsa_msg_done`.
const bool wait_for_first_byte_before_msg_done_ = Runtime::runtimeFeatureEnabled(
"envoy.reloadable_features.wait_for_first_byte_before_balsa_msg_done");
};

} // namespace Http1
Expand Down
1 change: 1 addition & 0 deletions source/common/runtime/runtime_features.cc
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ RUNTIME_GUARD(envoy_reloadable_features_use_typed_metadata_in_proxy_protocol_lis
RUNTIME_GUARD(envoy_reloadable_features_validate_connect);
RUNTIME_GUARD(envoy_reloadable_features_validate_grpc_header_before_log_grpc_status);
RUNTIME_GUARD(envoy_reloadable_features_validate_upstream_headers);
RUNTIME_GUARD(envoy_reloadable_features_wait_for_first_byte_before_balsa_msg_done);
RUNTIME_GUARD(envoy_reloadable_features_xds_failover_to_primary_enabled);
RUNTIME_GUARD(envoy_reloadable_features_xdstp_path_avoid_colon_encoding);
RUNTIME_GUARD(envoy_restart_features_allow_client_socket_creation_failure);
Expand Down
84 changes: 84 additions & 0 deletions test/integration/protocol_integration_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1557,6 +1557,90 @@ TEST_P(ProtocolIntegrationTest, EnvoyProxying104) {
testEnvoyProxying1xx(false, false, false, "104");
}

TEST_P(DownstreamProtocolIntegrationTest, EnvoyProxying102DelayBalsaReset) {
if (GetParam().http1_implementation != Http1ParserImpl::BalsaParser ||
GetParam().upstream_protocol != Http::CodecType::HTTP1 ||
GetParam().downstream_protocol != Http::CodecType::HTTP1) {
GTEST_SKIP() << "This test is only relevant for HTTP1 BalsaParser";
}
config_helper_.addConfigModifier(
[&](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager&
hcm) -> void { hcm.set_proxy_100_continue(true); });
config_helper_.addRuntimeOverride(
"envoy.reloadable_features.wait_for_first_byte_before_balsa_msg_done", "false");
config_helper_.addRuntimeOverride("envoy.reloadable_features.http1_balsa_delay_reset", "true");
initialize();

codec_client_ = makeHttpConnection(lookupPort("http"));
auto response = codec_client_->makeHeaderOnlyRequest(
Http::TestRequestHeaderMapImpl{{":method", "HEAD"},
{":path", "/dynamo/url"},
{":scheme", "http"},
{":authority", "sni.lyft.com"},
{"expect", "100-contINUE"}});
waitForNextUpstreamRequest();
upstream_request_->encode1xxHeaders(Http::TestResponseHeaderMapImpl{{":status", "102"}});
response->waitFor1xxHeaders();
upstream_request_->encodeHeaders(default_response_headers_, true);

EXPECT_FALSE(response->waitForEndStream());

cleanupUpstreamAndDownstream();
}

TEST_P(DownstreamProtocolIntegrationTest, EnvoyProxying102DelayBalsaResetWaitForFirstByte) {
if (GetParam().http1_implementation != Http1ParserImpl::BalsaParser ||
GetParam().upstream_protocol != Http::CodecType::HTTP1) {
GTEST_SKIP() << "This test is only relevant for HTTP1 upstream with BalsaParser";
}
config_helper_.addConfigModifier(
[&](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager&
hcm) -> void { hcm.set_proxy_100_continue(true); });
config_helper_.addRuntimeOverride(
"envoy.reloadable_features.wait_for_first_byte_before_balsa_msg_done", "true");
config_helper_.addRuntimeOverride("envoy.reloadable_features.http1_balsa_delay_reset", "true");
initialize();

codec_client_ = makeHttpConnection(lookupPort("http"));
auto response = codec_client_->makeHeaderOnlyRequest(
Http::TestRequestHeaderMapImpl{{":method", "HEAD"},
{":path", "/dynamo/url"},
{":scheme", "http"},
{":authority", "sni.lyft.com"},
{"expect", "100-contINUE"}});
waitForNextUpstreamRequest();
upstream_request_->encode1xxHeaders(Http::TestResponseHeaderMapImpl{{":status", "102"}});
response->waitFor1xxHeaders();
upstream_request_->encodeHeaders(default_response_headers_, true);
ASSERT_TRUE(response->waitForEndStream());
}

TEST_P(DownstreamProtocolIntegrationTest, EnvoyProxying102NoDelayBalsaReset) {
if (GetParam().http1_implementation != Http1ParserImpl::BalsaParser ||
GetParam().upstream_protocol != Http::CodecType::HTTP1) {
GTEST_SKIP() << "This test is only relevant for HTTP1 upstream with BalsaParser";
}
config_helper_.addConfigModifier(
[&](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager&
hcm) -> void { hcm.set_proxy_100_continue(true); });
config_helper_.addRuntimeOverride("envoy.reloadable_features.http1_balsa_delay_reset", "false");
initialize();

codec_client_ = makeHttpConnection(lookupPort("http"));
auto response = codec_client_->makeHeaderOnlyRequest(
Http::TestRequestHeaderMapImpl{{":method", "HEAD"},
{":path", "/dynamo/url"},
{":scheme", "http"},
{":authority", "sni.lyft.com"},
{"expect", "100-contINUE"}});

waitForNextUpstreamRequest();
upstream_request_->encode1xxHeaders(Http::TestResponseHeaderMapImpl{{":status", "102"}});
response->waitFor1xxHeaders();
upstream_request_->encodeHeaders(default_response_headers_, true);
ASSERT_TRUE(response->waitForEndStream());
}

TEST_P(ProtocolIntegrationTest, TwoRequests) { testTwoRequests(); }

TEST_P(ProtocolIntegrationTest, TwoRequestsWithForcedBackup) { testTwoRequests(true); }
Expand Down

0 comments on commit c908963

Please sign in to comment.