Skip to content

Commit c380f76

Browse files
authored
ext_authz: fixes a bug in the retry_policy to respective user's config (#42232)
1 parent 3d963b0 commit c380f76

File tree

4 files changed

+190
-2
lines changed

4 files changed

+190
-2
lines changed

changelogs/current.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,15 @@ minor_behavior_changes:
6363
change: |
6464
Check that the response header count and size is less than the configured limits after applying
6565
mutations and send a local reply if not.
66+
- area: ext_authz
67+
change: |
68+
Fixed HTTP ext_authz client to correctly respect user-configured ``retry_on`` configuration in the
69+
:ref:`retry_policy <envoy_v3_api_field_config.core.v3.RetryPolicy.retry_on>`. Previously, the
70+
configured ``retry_on`` value was being overridden with hardcoded defaults
71+
``5xx,gateway-error,connect-failure,reset``, causing user-specified retry conditions to be ignored.
72+
This behavior is controlled by the runtime flag
73+
``envoy.reloadable_features.ext_authz_http_client_retries_respect_user_retry_on`` which defaults to
74+
``true``. To preserve the old behavior, set this flag to ``false``.
6675
- area: ext_authz
6776
change: |
6877
Fixed HTTP ext_authz service to properly propagate headers (such as ``set-cookie``) back to clients. The

source/common/runtime/runtime_features.cc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ RUNTIME_GUARD(envoy_reloadable_features_disallow_quic_client_udp_mmsg);
4141
RUNTIME_GUARD(envoy_reloadable_features_enable_cel_regex_precompilation);
4242
RUNTIME_GUARD(envoy_reloadable_features_enable_compression_bomb_protection);
4343
RUNTIME_GUARD(envoy_reloadable_features_enable_new_query_param_present_match_behavior);
44+
RUNTIME_GUARD(envoy_reloadable_features_ext_authz_http_client_retries_respect_user_retry_on);
4445
// Ignore the automated "remove this flag" issue: we should keep this for 1 year. Confirm with
4546
// @yanjunxiang-google before removing.
4647
RUNTIME_GUARD(envoy_reloadable_features_ext_proc_fail_close_spurious_resp);

source/extensions/filters/common/ext_authz/ext_authz_http_impl.cc

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,16 @@ absl::StatusOr<Router::RetryPolicyConstSharedPtr>
112112
createRetryPolicy(const envoy::config::core::v3::RetryPolicy& core_retry_policy,
113113
Server::Configuration::CommonFactoryContext& context) {
114114
// Convert core retry policy to route retry policy and create the implementation.
115+
// By default when runtime flag is true, pass empty string to respect user's configured
116+
// retry_on, not override it. When flag is false, use hardcoded defaults for backwards
117+
// compatibility.
118+
const std::string default_retry_on =
119+
Runtime::runtimeFeatureEnabled(
120+
"envoy.reloadable_features.ext_authz_http_client_retries_respect_user_retry_on")
121+
? ""
122+
: "5xx,gateway-error,connect-failure,reset";
115123
envoy::config::route::v3::RetryPolicy route_retry_policy =
116-
Http::Utility::convertCoreToRouteRetryPolicy(core_retry_policy,
117-
"5xx,gateway-error,connect-failure,reset");
124+
Http::Utility::convertCoreToRouteRetryPolicy(core_retry_policy, default_retry_on);
118125

119126
return Router::RetryPolicyImpl::create(route_retry_policy, context.messageValidationVisitor(),
120127
context);

test/extensions/filters/http/ext_authz/ext_authz_integration_test.cc

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1777,6 +1777,177 @@ TEST_P(ExtAuthzHttpIntegrationTest, HttpRetryPolicy) {
17771777
cleanup();
17781778
}
17791779

1780+
// Test that user-configured retry_on conditions are respected in HTTP ext_authz.
1781+
TEST_P(ExtAuthzHttpIntegrationTest, HttpRetryPolicyRespectedNotOverridden) {
1782+
config_helper_.addConfigModifier([this](envoy::config::bootstrap::v3::Bootstrap& bootstrap) {
1783+
auto* ext_authz_cluster = bootstrap.mutable_static_resources()->add_clusters();
1784+
ext_authz_cluster->MergeFrom(bootstrap.static_resources().clusters()[0]);
1785+
ext_authz_cluster->set_name("ext_authz");
1786+
1787+
// Configure retry_on with "retriable-4xx" which is not one of the hardcoded values we were
1788+
// setting previously, "5xx,gateway-error,connect-failure,reset".
1789+
// Before the fix, this would be ignored and only 5xx/gateway-error/etc would trigger retries.
1790+
// After the fix, 4XX should trigger a retry as well.
1791+
const std::string ext_authz_config = R"EOF(
1792+
http_service:
1793+
server_uri:
1794+
uri: "ext_authz:9000"
1795+
cluster: "ext_authz"
1796+
timeout: 300s
1797+
authorization_response:
1798+
allowed_upstream_headers:
1799+
patterns:
1800+
- exact: baz
1801+
retry_policy:
1802+
retry_on: "retriable-4xx"
1803+
num_retries: 1
1804+
retry_back_off:
1805+
base_interval: 0.01s
1806+
max_interval: 0.1s
1807+
failure_mode_allow: false
1808+
)EOF";
1809+
TestUtility::loadFromYaml(ext_authz_config, proto_config_);
1810+
proto_config_.set_encode_raw_headers(encodeRawHeaders());
1811+
1812+
envoy::extensions::filters::network::http_connection_manager::v3::HttpFilter ext_authz_filter;
1813+
ext_authz_filter.set_name("envoy.filters.http.ext_authz");
1814+
ext_authz_filter.mutable_typed_config()->PackFrom(proto_config_);
1815+
1816+
config_helper_.prependFilter(MessageUtil::getJsonStringFromMessageOrError(ext_authz_filter));
1817+
});
1818+
1819+
HttpIntegrationTest::initialize();
1820+
1821+
auto conn = makeClientConnection(lookupPort("http"));
1822+
codec_client_ = makeHttpConnection(std::move(conn));
1823+
const auto headers = Http::TestRequestHeaderMapImpl{
1824+
{":method", "GET"}, {":path", "/"}, {":scheme", "http"}, {":authority", "host"}};
1825+
response_ = codec_client_->makeHeaderOnlyRequest(headers);
1826+
1827+
// Wait for the first ext_authz request.
1828+
AssertionResult result =
1829+
fake_upstreams_.back()->waitForHttpConnection(*dispatcher_, fake_ext_authz_connection_);
1830+
RELEASE_ASSERT(result, result.message());
1831+
result = fake_ext_authz_connection_->waitForNewStream(*dispatcher_, ext_authz_request_);
1832+
RELEASE_ASSERT(result, result.message());
1833+
result = ext_authz_request_->waitForEndStream(*dispatcher_);
1834+
RELEASE_ASSERT(result, result.message());
1835+
1836+
// Send a 409 Conflict error response to trigger retry (retriable-4xx).
1837+
// Before the fixes we made, no retry happens because "retriable-4xx" was overridden by the
1838+
// hardcoded defaults. After the fix, retry should happen because user's "retriable-4xx"
1839+
// gets respected and not overridden.
1840+
Http::TestResponseHeaderMapImpl error_response_headers{{":status", "409"}};
1841+
ext_authz_request_->encodeHeaders(error_response_headers, true);
1842+
1843+
// Wait for the retry request to the ext_authz server.
1844+
// This will TIMEOUT before the fixes we made, and SUCCEED after the fixes.
1845+
FakeStreamPtr ext_authz_retry_request;
1846+
result = fake_ext_authz_connection_->waitForNewStream(*dispatcher_, ext_authz_retry_request);
1847+
RELEASE_ASSERT(result, result.message());
1848+
result = ext_authz_retry_request->waitForEndStream(*dispatcher_);
1849+
RELEASE_ASSERT(result, result.message());
1850+
1851+
// Send a successful response on the retry.
1852+
Http::TestResponseHeaderMapImpl success_response_headers{
1853+
{":status", "200"},
1854+
{"baz", "test-value"},
1855+
};
1856+
ext_authz_retry_request->encodeHeaders(success_response_headers, true);
1857+
1858+
// The request should now proceed to upstream.
1859+
result = fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_);
1860+
RELEASE_ASSERT(result, result.message());
1861+
result = fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_);
1862+
RELEASE_ASSERT(result, result.message());
1863+
result = upstream_request_->waitForEndStream(*dispatcher_);
1864+
RELEASE_ASSERT(result, result.message());
1865+
1866+
// Verify the request was modified by the successful ext_authz response.
1867+
EXPECT_THAT(upstream_request_->headers(), ContainsHeader("baz", "test-value"));
1868+
1869+
upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, true);
1870+
ASSERT_TRUE(response_->waitForEndStream());
1871+
EXPECT_TRUE(response_->complete());
1872+
EXPECT_EQ("200", response_->headers().getStatusValue());
1873+
1874+
cleanup();
1875+
}
1876+
1877+
// Test that when the runtime flag is disabled, we preserve the old behavior of overriding
1878+
// user-configured retry_on with hardcoded defaults.
1879+
TEST_P(ExtAuthzHttpIntegrationTest, HttpRetryPolicyOldBehaviorWithFlagDisabled) {
1880+
// Disable the runtime flag to test old behavior.
1881+
config_helper_.addRuntimeOverride(
1882+
"envoy.reloadable_features.ext_authz_http_client_retries_respect_user_retry_on", "false");
1883+
1884+
config_helper_.addConfigModifier([this](envoy::config::bootstrap::v3::Bootstrap& bootstrap) {
1885+
auto* ext_authz_cluster = bootstrap.mutable_static_resources()->add_clusters();
1886+
ext_authz_cluster->MergeFrom(bootstrap.static_resources().clusters()[0]);
1887+
ext_authz_cluster->set_name("ext_authz");
1888+
1889+
// Configure retry_on with "retriable-4xx" which should be ignored in old behavior.
1890+
// With the flag disabled, the hardcoded defaults "5xx,gateway-error,connect-failure,reset"
1891+
// override the user config, so 409 (4xx) should NOT trigger a retry.
1892+
const std::string ext_authz_config = R"EOF(
1893+
http_service:
1894+
server_uri:
1895+
uri: "ext_authz:9000"
1896+
cluster: "ext_authz"
1897+
timeout: 300s
1898+
authorization_response:
1899+
allowed_upstream_headers:
1900+
patterns:
1901+
- exact: baz
1902+
retry_policy:
1903+
retry_on: "retriable-4xx"
1904+
num_retries: 1
1905+
retry_back_off:
1906+
base_interval: 0.01s
1907+
max_interval: 0.1s
1908+
failure_mode_allow: false
1909+
)EOF";
1910+
TestUtility::loadFromYaml(ext_authz_config, proto_config_);
1911+
proto_config_.set_encode_raw_headers(encodeRawHeaders());
1912+
1913+
envoy::extensions::filters::network::http_connection_manager::v3::HttpFilter ext_authz_filter;
1914+
ext_authz_filter.set_name("envoy.filters.http.ext_authz");
1915+
ext_authz_filter.mutable_typed_config()->PackFrom(proto_config_);
1916+
1917+
config_helper_.prependFilter(MessageUtil::getJsonStringFromMessageOrError(ext_authz_filter));
1918+
});
1919+
1920+
HttpIntegrationTest::initialize();
1921+
1922+
auto conn = makeClientConnection(lookupPort("http"));
1923+
codec_client_ = makeHttpConnection(std::move(conn));
1924+
const auto headers = Http::TestRequestHeaderMapImpl{
1925+
{":method", "GET"}, {":path", "/"}, {":scheme", "http"}, {":authority", "host"}};
1926+
response_ = codec_client_->makeHeaderOnlyRequest(headers);
1927+
1928+
// Wait for the first ext_authz request.
1929+
AssertionResult result =
1930+
fake_upstreams_.back()->waitForHttpConnection(*dispatcher_, fake_ext_authz_connection_);
1931+
RELEASE_ASSERT(result, result.message());
1932+
result = fake_ext_authz_connection_->waitForNewStream(*dispatcher_, ext_authz_request_);
1933+
RELEASE_ASSERT(result, result.message());
1934+
result = ext_authz_request_->waitForEndStream(*dispatcher_);
1935+
RELEASE_ASSERT(result, result.message());
1936+
1937+
// Send a 409 Conflict error response.
1938+
// With the flag disabled (old behavior), the user's "retriable-4xx" is overridden by
1939+
// hardcoded defaults, so NO retry should happen for this 4xx response.
1940+
Http::TestResponseHeaderMapImpl error_response_headers{{":status", "409"}};
1941+
ext_authz_request_->encodeHeaders(error_response_headers, true);
1942+
1943+
// The client should receive the denial response directly without any retry.
1944+
ASSERT_TRUE(response_->waitForEndStream());
1945+
EXPECT_TRUE(response_->complete());
1946+
EXPECT_EQ("409", response_->headers().getStatusValue());
1947+
1948+
cleanup();
1949+
}
1950+
17801951
class ExtAuthzLocalReplyIntegrationTest : public HttpIntegrationTest,
17811952
public TestWithParam<Network::Address::IpVersion> {
17821953
public:

0 commit comments

Comments
 (0)