Skip to content

Commit 459f387

Browse files
authored
router: fixes a regression where headers were not available in request_headers_to_add (#41801)
## Description This PR fixes a regression where router-set headers like `x-envoy-expected-rq-timeout-ms`, `x-envoy-attempt-count`, etc. were not accessible in `request_headers_to_add` configuration. It happened as a side effect of #39617 which moved the call to `route_entry_->finalizeRequestHeaders()` earlier in the request processing workflow, which changed when `request_headers_to_add` is processed. Fix #41794 --- **Commit Message:** router: fixes a regression where headers were not available in request_headers_to_add **Additional Description:** Fixes a regression where router-set headers like `x-envoy-expected-rq-timeout-ms`, `x-envoy-attempt-count`, etc. were not accessible in `request_headers_to_add` configuration. **Risk Level:** Low **Testing:** Added Unit + Integration Tests **Docs Changes:** Added **Release Notes:** Added --------- Signed-off-by: Rohit Agrawal <[email protected]>
1 parent eaab311 commit 459f387

File tree

6 files changed

+127
-14
lines changed

6 files changed

+127
-14
lines changed

changelogs/current.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@ bug_fixes:
8282
change: |
8383
c-ares resolver: add optional ``reinit_channel_on_timeout`` to reinitialize
8484
the resolver after DNS timeouts.
85+
- area: router
86+
change: |
87+
Fixed a regression where router-set headers (e.g., ``x-envoy-expected-rq-timeout-ms``, ``x-envoy-attempt-count``)
88+
were not accessible in ``request_headers_to_add`` configuration on the initial request. Headers configured via
89+
``request_headers_to_add`` can now reference router-set headers using formatters like
90+
``%REQ(x-envoy-expected-rq-timeout-ms)%``.
8591
8692
removed_config_or_runtime:
8793
# *Normally occurs at the end of the* :ref:`deprecation period <deprecated>`

source/common/router/config_impl.cc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -906,6 +906,8 @@ void RouteEntryImplBase::finalizeRequestHeaders(Http::RequestHeaderMap& headers,
906906
const Formatter::Context& context,
907907
const StreamInfo::StreamInfo& stream_info,
908908
bool keep_original_host_or_path) const {
909+
// Apply header transformations configured via request_headers_to_add first.
910+
// This is important because host/path rewriting may depend on headers added here.
909911
for (const HeaderParser* header_parser : getRequestHeaderParsers(
910912
/*specificity_ascend=*/vhost_->globalRouteConfig().mostSpecificHeaderMutationsWins())) {
911913
// Later evaluated header parser wins.

source/common/router/config_impl.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,7 @@ class RouteEntryImplBase : public RouteEntryAndRoute,
612612
void finalizeRequestHeaders(Http::RequestHeaderMap& headers, const Formatter::Context& context,
613613
const StreamInfo::StreamInfo& stream_info,
614614
bool keep_original_host_or_path) const override;
615+
615616
Http::HeaderTransforms requestHeaderTransforms(const StreamInfo::StreamInfo& stream_info,
616617
bool do_formatting = true) const override;
617618
void finalizeResponseHeaders(Http::ResponseHeaderMap& headers, const Formatter::Context& context,

source/common/router/router.cc

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -600,7 +600,24 @@ Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers,
600600
attempt_count_++;
601601
callbacks_->streamInfo().setAttemptCount(attempt_count_);
602602

603-
// Finalize the request headers before the host selection.
603+
// Set hedging params before finalizeRequestHeaders so timeout calculation is correct.
604+
hedging_params_ = FilterUtility::finalHedgingParams(*route_entry_, headers);
605+
606+
// Calculate timeout and set x-envoy-expected-rq-timeout-ms before finalizeRequestHeaders.
607+
// This allows request_headers_to_add to reference the timeout header.
608+
timeout_ = FilterUtility::finalTimeout(*route_entry_, headers, !config_->suppress_envoy_headers_,
609+
grpc_request_, hedging_params_.hedge_on_per_try_timeout_,
610+
config_->respect_expected_rq_timeout_);
611+
612+
// Set x-envoy-attempt-count before finalizeRequestHeaders so it can be referenced.
613+
include_attempt_count_in_request_ = route_entry_->includeAttemptCountInRequest();
614+
if (include_attempt_count_in_request_) {
615+
headers.setEnvoyAttemptCount(attempt_count_);
616+
}
617+
618+
// Finalize request headers (host/path rewriting + request_headers_to_add) before host selection.
619+
// At this point, router-set headers (x-envoy-expected-rq-timeout-ms, x-envoy-attempt-count)
620+
// are already set, so request_headers_to_add can reference them and affect load balancing.
604621
const Formatter::Context formatter_context(&headers, {}, {}, {}, {}, &callbacks_->activeSpan());
605622
route_entry_->finalizeRequestHeaders(headers, formatter_context, callbacks_->streamInfo(),
606623
!config_->suppress_envoy_headers_);
@@ -755,12 +772,7 @@ bool Filter::continueDecodeHeaders(Upstream::ThreadLocalCluster* cluster,
755772
return false;
756773
}
757774

758-
hedging_params_ = FilterUtility::finalHedgingParams(*route_entry_, headers);
759-
760-
timeout_ = FilterUtility::finalTimeout(*route_entry_, headers, !config_->suppress_envoy_headers_,
761-
grpc_request_, hedging_params_.hedge_on_per_try_timeout_,
762-
config_->respect_expected_rq_timeout_);
763-
775+
// Handle additional header processing.
764776
const Http::HeaderEntry* header_max_stream_duration_entry =
765777
headers.EnvoyUpstreamStreamDurationMs();
766778
if (header_max_stream_duration_entry) {
@@ -769,17 +781,12 @@ bool Filter::continueDecodeHeaders(Upstream::ThreadLocalCluster* cluster,
769781
headers.removeEnvoyUpstreamStreamDurationMs();
770782
}
771783

772-
// If this header is set with any value, use an alternate response code on timeout
784+
// If this header is set with any value, use an alternate response code on timeout.
773785
if (headers.EnvoyUpstreamRequestTimeoutAltResponse()) {
774786
timeout_response_code_ = Http::Code::NoContent;
775787
headers.removeEnvoyUpstreamRequestTimeoutAltResponse();
776788
}
777789

778-
include_attempt_count_in_request_ = route_entry_->includeAttemptCountInRequest();
779-
if (include_attempt_count_in_request_) {
780-
headers.setEnvoyAttemptCount(attempt_count_);
781-
}
782-
783790
FilterUtility::setUpstreamScheme(
784791
headers, callbacks_->streamInfo().downstreamAddressProvider().sslConnection() != nullptr,
785792
host->transportSocketFactory().sslCtx() != nullptr,
@@ -2241,7 +2248,7 @@ void Filter::continueDoRetry(bool can_send_early_data, bool can_use_http3,
22412248
}
22422249

22432250
// The request timeouts only account for time elapsed since the downstream request completed
2244-
// which might not have happened yet (in which case zero time has elapsed.)
2251+
// which might not have happened yet, in which case zero time has elapsed.
22452252
std::chrono::milliseconds elapsed_time = std::chrono::milliseconds::zero();
22462253

22472254
if (DateUtil::timePointValid(downstream_request_complete_time_)) {

test/common/router/config_impl_test.cc

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7812,6 +7812,48 @@ TEST_F(CustomRequestHeadersTest, CustomHeaderWrongFormat) {
78127812
EnvoyException);
78137813
}
78147814

7815+
// Validate that request_headers_to_add can reference router-set headers like
7816+
// x-envoy-expected-rq-timeout-ms and x-envoy-attempt-count on the initial request.
7817+
TEST_F(CustomRequestHeadersTest, RequestHeadersCanReferenceRouterSetHeaders) {
7818+
const std::string yaml = R"EOF(
7819+
virtual_hosts:
7820+
- name: www2
7821+
domains:
7822+
- www.lyft.com
7823+
routes:
7824+
- match:
7825+
prefix: "/endpoint"
7826+
route:
7827+
cluster: www2
7828+
timeout: 5s
7829+
request_headers_to_add:
7830+
- header:
7831+
key: x-timeout-copy
7832+
value: "%REQ(x-envoy-expected-rq-timeout-ms)%"
7833+
- header:
7834+
key: x-attempt-copy
7835+
value: "%REQ(x-envoy-attempt-count)%"
7836+
)EOF";
7837+
NiceMock<Envoy::StreamInfo::MockStreamInfo> stream_info;
7838+
factory_context_.cluster_manager_.initializeClusters({"www2"}, {});
7839+
TestConfigImpl config(parseRouteConfigurationFromYaml(yaml), factory_context_, true,
7840+
creation_status_);
7841+
Http::TestRequestHeaderMapImpl headers = genHeaders("www.lyft.com", "/endpoint", "GET");
7842+
const RouteEntry* route_entry = config.route(headers, 0)->routeEntry();
7843+
7844+
// Simulate router filter setting these headers before calling finalizeRequestHeaders.
7845+
// This mimics the fix where router-set headers are added before finalizeRequestHeaders.
7846+
headers.addCopy(Http::LowerCaseString("x-envoy-expected-rq-timeout-ms"), "5000");
7847+
headers.addCopy(Http::LowerCaseString("x-envoy-attempt-count"), "1");
7848+
7849+
auto formatter_context = Formatter::Context().setRequestHeaders(headers);
7850+
route_entry->finalizeRequestHeaders(headers, formatter_context, stream_info, true);
7851+
7852+
// Verify that request_headers_to_add was able to reference router-set headers.
7853+
EXPECT_EQ("5000", headers.get_("x-timeout-copy"));
7854+
EXPECT_EQ("1", headers.get_("x-attempt-copy"));
7855+
}
7856+
78157857
TEST(MetadataMatchCriteriaImpl, Create) {
78167858
auto v1 = Protobuf::Value();
78177859
v1.set_string_value("v1");

test/common/router/router_test.cc

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1327,6 +1327,61 @@ TEST_F(RouterTest, EnvoyAttemptCountInResponseNotOverwritten) {
13271327
/* expected_count */ 123);
13281328
}
13291329

1330+
// Validate that router-set headers like x-envoy-expected-rq-timeout-ms are accessible in
1331+
// request_headers_to_add configuration.
1332+
TEST_F(RouterTest, RouterSetHeadersAccessibleInRequestHeadersToAdd) {
1333+
NiceMock<Http::MockRequestEncoder> encoder;
1334+
Http::ResponseDecoder* response_decoder = nullptr;
1335+
1336+
EXPECT_CALL(cm_.thread_local_cluster_.conn_pool_, newStream(_, _, _))
1337+
.WillOnce(
1338+
Invoke([&](Http::ResponseDecoder& decoder, Http::ConnectionPool::Callbacks& callbacks,
1339+
const Http::ConnectionPool::Instance::StreamOptions&)
1340+
-> Http::ConnectionPool::Cancellable* {
1341+
response_decoder = &decoder;
1342+
callbacks.onPoolReady(encoder, cm_.thread_local_cluster_.conn_pool_.host_,
1343+
upstream_stream_info_, Http::Protocol::Http10);
1344+
return nullptr;
1345+
}));
1346+
1347+
expectResponseTimerCreate();
1348+
1349+
// Set up finalizeRequestHeaders to simulate request_headers_to_add with a reference to
1350+
// x-envoy-expected-rq-timeout-ms. This will be called AFTER router-set headers are added.
1351+
EXPECT_CALL(callbacks_.route_->route_entry_, finalizeRequestHeaders(_, _, _, _))
1352+
.WillOnce(Invoke([](Http::RequestHeaderMap& headers, const Formatter::Context&,
1353+
const StreamInfo::StreamInfo&, bool) {
1354+
// Simulate request_headers_to_add configuration:
1355+
// - header:
1356+
// key: x-timeout
1357+
// value: '%REQ(x-envoy-expected-rq-timeout-ms)%'
1358+
// append_action: ADD_IF_ABSENT
1359+
const auto timeout_header =
1360+
headers.get(Http::LowerCaseString("x-envoy-expected-rq-timeout-ms"));
1361+
if (!timeout_header.empty()) {
1362+
headers.addCopy(Http::LowerCaseString("x-timeout"),
1363+
timeout_header[0]->value().getStringView());
1364+
}
1365+
}));
1366+
1367+
Http::TestRequestHeaderMapImpl headers;
1368+
HttpTestUtility::addDefaultHeaders(headers);
1369+
router_->decodeHeaders(headers, true);
1370+
1371+
// Verify that x-envoy-expected-rq-timeout-ms was set by the router.
1372+
EXPECT_FALSE(headers.get_("x-envoy-expected-rq-timeout-ms").empty());
1373+
1374+
// Verify that our request_headers_to_add logic was able to copy it to x-timeout.
1375+
// This verifies the fix: finalizeRequestHeaders is called AFTER router-set headers.
1376+
EXPECT_FALSE(headers.get_("x-timeout").empty());
1377+
EXPECT_EQ(headers.get_("x-envoy-expected-rq-timeout-ms"), headers.get_("x-timeout"));
1378+
1379+
Http::ResponseHeaderMapPtr response_headers(
1380+
new Http::TestResponseHeaderMapImpl{{":status", "200"}});
1381+
response_decoder->decodeHeaders(std::move(response_headers), true);
1382+
EXPECT_TRUE(verifyHostUpstreamStats(1, 0));
1383+
}
1384+
13301385
// Validate that x-envoy-attempt-count is present in local replies after an upstream attempt is
13311386
// made.
13321387
TEST_F(RouterTest, EnvoyAttemptCountInResponsePresentWithLocalReply) {

0 commit comments

Comments
 (0)