@@ -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+
17801951class ExtAuthzLocalReplyIntegrationTest : public HttpIntegrationTest ,
17811952 public TestWithParam<Network::Address::IpVersion> {
17821953public:
0 commit comments