diff --git a/api/envoy/extensions/filters/http/ratelimit/v3/rate_limit.proto b/api/envoy/extensions/filters/http/ratelimit/v3/rate_limit.proto index 326a30ef4013..bc58e7f9b2e1 100644 --- a/api/envoy/extensions/filters/http/ratelimit/v3/rate_limit.proto +++ b/api/envoy/extensions/filters/http/ratelimit/v3/rate_limit.proto @@ -19,7 +19,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // Rate limit :ref:`configuration overview `. // [#extension: envoy.filters.http.ratelimit] -// [#next-free-field: 9] +// [#next-free-field: 10] message RateLimit { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.http.rate_limit.v2.RateLimit"; @@ -60,7 +60,6 @@ message RateLimit { // The filter's behaviour in case the rate limiting service does // not respond back. When it is set to true, Envoy will not allow traffic in case of // communication failure between rate limiting service and the proxy. - // Defaults to false. bool failure_mode_deny = 5; // Specifies whether a `RESOURCE_EXHAUSTED` gRPC code must be returned instead @@ -99,6 +98,11 @@ message RateLimit { // Disabled by default. XRateLimitHeadersRFCVersion enable_x_ratelimit_headers = 8 [(validate.rules).enum = {defined_only: true}]; + + // Disables emitting the :ref:`x-envoy-ratelimited` header + // in case of rate limiting (i.e. 429 responses). + // Having this header not present potentially makes the request retriable. + bool disable_x_envoy_ratelimited_header = 9; } message RateLimitPerRoute { diff --git a/docs/root/configuration/http/http_filters/rate_limit_filter.rst b/docs/root/configuration/http/http_filters/rate_limit_filter.rst index 91ce997c72cd..0896f9a5b86d 100644 --- a/docs/root/configuration/http/http_filters/rate_limit_filter.rst +++ b/docs/root/configuration/http/http_filters/rate_limit_filter.rst @@ -14,7 +14,9 @@ can optionally include the virtual host rate limit configurations. More than one apply to a request. Each configuration results in a descriptor being sent to the rate limit service. If the rate limit service is called, and the response for any of the descriptors is over limit, a -429 response is returned. The rate limit filter also sets the :ref:`x-envoy-ratelimited` header. +429 response is returned. The rate limit filter also sets the :ref:`x-envoy-ratelimited` header, +unless :ref:`disable_x_envoy_ratelimited_header ` is +set to true. If there is an error in calling rate limit service or rate limit service returns an error and :ref:`failure_mode_deny ` is set to true, a 500 response is returned. diff --git a/docs/root/version_history/current.rst b/docs/root/version_history/current.rst index 977e3aaa8981..237f9ed5b10a 100644 --- a/docs/root/version_history/current.rst +++ b/docs/root/version_history/current.rst @@ -29,6 +29,7 @@ New Features * grpc: implemented header value syntax support when defining :ref:`initial metadata ` for gRPC-based `ext_authz` :ref:`HTTP ` and :ref:`network ` filters, and :ref:`ratelimit ` filters. * health_check: added option to use :ref:`no_traffic_healthy_interval ` which allows a different no traffic interval when the host is healthy. * mongo_proxy: the list of commands to produce metrics for is now :ref:`configurable `. +* ratelimit: added :ref:`disable_x_envoy_ratelimited_header ` option to disable `X-Envoy-RateLimited` header. * tcp: added a new :ref:`envoy.overload_actions.reject_incoming_connections ` action to reject incoming TCP connections. Deprecated diff --git a/generated_api_shadow/envoy/extensions/filters/http/ratelimit/v3/rate_limit.proto b/generated_api_shadow/envoy/extensions/filters/http/ratelimit/v3/rate_limit.proto index 326a30ef4013..bc58e7f9b2e1 100644 --- a/generated_api_shadow/envoy/extensions/filters/http/ratelimit/v3/rate_limit.proto +++ b/generated_api_shadow/envoy/extensions/filters/http/ratelimit/v3/rate_limit.proto @@ -19,7 +19,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // Rate limit :ref:`configuration overview `. // [#extension: envoy.filters.http.ratelimit] -// [#next-free-field: 9] +// [#next-free-field: 10] message RateLimit { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.http.rate_limit.v2.RateLimit"; @@ -60,7 +60,6 @@ message RateLimit { // The filter's behaviour in case the rate limiting service does // not respond back. When it is set to true, Envoy will not allow traffic in case of // communication failure between rate limiting service and the proxy. - // Defaults to false. bool failure_mode_deny = 5; // Specifies whether a `RESOURCE_EXHAUSTED` gRPC code must be returned instead @@ -99,6 +98,11 @@ message RateLimit { // Disabled by default. XRateLimitHeadersRFCVersion enable_x_ratelimit_headers = 8 [(validate.rules).enum = {defined_only: true}]; + + // Disables emitting the :ref:`x-envoy-ratelimited` header + // in case of rate limiting (i.e. 429 responses). + // Having this header not present potentially makes the request retriable. + bool disable_x_envoy_ratelimited_header = 9; } message RateLimitPerRoute { diff --git a/source/extensions/filters/http/ratelimit/ratelimit.cc b/source/extensions/filters/http/ratelimit/ratelimit.cc index c5fb0d284a49..8430f47243a8 100644 --- a/source/extensions/filters/http/ratelimit/ratelimit.cc +++ b/source/extensions/filters/http/ratelimit/ratelimit.cc @@ -170,11 +170,13 @@ void Filter::complete(Filters::Common::RateLimit::LimitStatus status, empty_stat_name, false}; httpContext().codeStats().chargeResponseStat(info); - if (response_headers_to_add_ == nullptr) { - response_headers_to_add_ = Http::ResponseHeaderMapImpl::create(); + if (config_->enableXEnvoyRateLimitedHeader()) { + if (response_headers_to_add_ == nullptr) { + response_headers_to_add_ = Http::ResponseHeaderMapImpl::create(); + } + response_headers_to_add_->setReferenceEnvoyRateLimited( + Http::Headers::get().EnvoyRateLimitedValues.True); } - response_headers_to_add_->setReferenceEnvoyRateLimited( - Http::Headers::get().EnvoyRateLimitedValues.True); break; } diff --git a/source/extensions/filters/http/ratelimit/ratelimit.h b/source/extensions/filters/http/ratelimit/ratelimit.h index 22fc9ca65f48..058eb793569a 100644 --- a/source/extensions/filters/http/ratelimit/ratelimit.h +++ b/source/extensions/filters/http/ratelimit/ratelimit.h @@ -51,6 +51,7 @@ class FilterConfig { enable_x_ratelimit_headers_( config.enable_x_ratelimit_headers() == envoy::extensions::filters::http::ratelimit::v3::RateLimit::DRAFT_VERSION_03), + disable_x_envoy_ratelimited_header_(config.disable_x_envoy_ratelimited_header()), rate_limited_grpc_status_( config.rate_limited_as_resource_exhausted() ? absl::make_optional(Grpc::Status::WellKnownGrpcStatus::ResourceExhausted) @@ -64,6 +65,7 @@ class FilterConfig { FilterRequestType requestType() const { return request_type_; } bool failureModeAllow() const { return !failure_mode_deny_; } bool enableXRateLimitHeaders() const { return enable_x_ratelimit_headers_; } + bool enableXEnvoyRateLimitedHeader() const { return !disable_x_envoy_ratelimited_header_; } const absl::optional rateLimitedGrpcStatus() const { return rate_limited_grpc_status_; } @@ -90,6 +92,7 @@ class FilterConfig { Runtime::Loader& runtime_; const bool failure_mode_deny_; const bool enable_x_ratelimit_headers_; + const bool disable_x_envoy_ratelimited_header_; const absl::optional rate_limited_grpc_status_; Http::Context& http_context_; Filters::Common::RateLimit::StatNames stat_names_; diff --git a/test/extensions/filters/http/ratelimit/ratelimit_integration_test.cc b/test/extensions/filters/http/ratelimit/ratelimit_integration_test.cc index 56114f530aca..e20d573b4a92 100644 --- a/test/extensions/filters/http/ratelimit/ratelimit_integration_test.cc +++ b/test/extensions/filters/http/ratelimit/ratelimit_integration_test.cc @@ -46,6 +46,7 @@ class RatelimitIntegrationTest : public Grpc::VersionedGrpcClientIntegrationPara TestUtility::loadFromYaml(base_filter_config_, proto_config_); proto_config_.set_failure_mode_deny(failure_mode_deny_); proto_config_.set_enable_x_ratelimit_headers(enable_x_ratelimit_headers_); + proto_config_.set_disable_x_envoy_ratelimited_header(disable_x_envoy_ratelimited_header_); setGrpcService(*proto_config_.mutable_rate_limit_service()->mutable_grpc_service(), "ratelimit", fake_upstreams_.back()->localAddress()); proto_config_.mutable_rate_limit_service()->set_transport_api_version(apiVersion()); @@ -192,6 +193,7 @@ class RatelimitIntegrationTest : public Grpc::VersionedGrpcClientIntegrationPara bool failure_mode_deny_ = false; envoy::extensions::filters::http::ratelimit::v3::RateLimit::XRateLimitHeadersRFCVersion enable_x_ratelimit_headers_ = envoy::extensions::filters::http::ratelimit::v3::RateLimit::OFF; + bool disable_x_envoy_ratelimited_header_ = false; envoy::extensions::filters::http::ratelimit::v3::RateLimit proto_config_{}; const std::string base_filter_config_ = R"EOF( domain: some_domain @@ -214,12 +216,24 @@ class RatelimitFilterHeadersEnabledIntegrationTest : public RatelimitIntegration } }; +// Test verifies that disabling X-Envoy-RateLimited response header works. +class RatelimitFilterEnvoyRatelimitedHeaderDisabledIntegrationTest + : public RatelimitIntegrationTest { +public: + RatelimitFilterEnvoyRatelimitedHeaderDisabledIntegrationTest() { + disable_x_envoy_ratelimited_header_ = true; + } +}; + INSTANTIATE_TEST_SUITE_P(IpVersionsClientType, RatelimitIntegrationTest, VERSIONED_GRPC_CLIENT_INTEGRATION_PARAMS); INSTANTIATE_TEST_SUITE_P(IpVersionsClientType, RatelimitFailureModeIntegrationTest, VERSIONED_GRPC_CLIENT_INTEGRATION_PARAMS); INSTANTIATE_TEST_SUITE_P(IpVersionsClientType, RatelimitFilterHeadersEnabledIntegrationTest, VERSIONED_GRPC_CLIENT_INTEGRATION_PARAMS); +INSTANTIATE_TEST_SUITE_P(IpVersionsClientType, + RatelimitFilterEnvoyRatelimitedHeaderDisabledIntegrationTest, + VERSIONED_GRPC_CLIENT_INTEGRATION_PARAMS); TEST_P(RatelimitIntegrationTest, Ok) { basicFlow(); } @@ -261,6 +275,11 @@ TEST_P(RatelimitIntegrationTest, OverLimit) { sendRateLimitResponse(envoy::service::ratelimit::v3::RateLimitResponse::OVER_LIMIT, {}, Http::TestResponseHeaderMapImpl{}, Http::TestRequestHeaderMapImpl{}); waitForFailedUpstreamResponse(429); + + EXPECT_THAT(response_.get()->headers(), + Http::HeaderValueOf(Http::Headers::get().EnvoyRateLimited, + Http::Headers::get().EnvoyRateLimitedValues.True)); + cleanup(); EXPECT_EQ(nullptr, test_server_->counter("cluster.cluster_0.ratelimit.ok")); @@ -284,6 +303,10 @@ TEST_P(RatelimitIntegrationTest, OverLimitWithHeaders) { return Http::HeaderMap::Iterate::Continue; }); + EXPECT_THAT(response_.get()->headers(), + Http::HeaderValueOf(Http::Headers::get().EnvoyRateLimited, + Http::Headers::get().EnvoyRateLimitedValues.True)); + cleanup(); EXPECT_EQ(nullptr, test_server_->counter("cluster.cluster_0.ratelimit.ok")); @@ -436,5 +459,23 @@ TEST_P(RatelimitFilterHeadersEnabledIntegrationTest, OverLimitWithFilterHeaders) EXPECT_EQ(nullptr, test_server_->counter("cluster.cluster_0.ratelimit.error")); } +TEST_P(RatelimitFilterEnvoyRatelimitedHeaderDisabledIntegrationTest, + OverLimitWithoutEnvoyRatelimitedHeader) { + initiateClientConnection(); + waitForRatelimitRequest(); + sendRateLimitResponse(envoy::service::ratelimit::v3::RateLimitResponse::OVER_LIMIT, {}, + Http::TestResponseHeaderMapImpl{}, Http::TestRequestHeaderMapImpl{}); + waitForFailedUpstreamResponse(429); + + EXPECT_THAT(response_.get()->headers(), + ::testing::Not(Http::HeaderValueOf(Http::Headers::get().EnvoyRateLimited, _))); + + cleanup(); + + EXPECT_EQ(nullptr, test_server_->counter("cluster.cluster_0.ratelimit.ok")); + EXPECT_EQ(1, test_server_->counter("cluster.cluster_0.ratelimit.over_limit")->value()); + EXPECT_EQ(nullptr, test_server_->counter("cluster.cluster_0.ratelimit.error")); +} + } // namespace } // namespace Envoy diff --git a/test/extensions/filters/http/ratelimit/ratelimit_test.cc b/test/extensions/filters/http/ratelimit/ratelimit_test.cc index c7ab957ada5e..4eff42850721 100644 --- a/test/extensions/filters/http/ratelimit/ratelimit_test.cc +++ b/test/extensions/filters/http/ratelimit/ratelimit_test.cc @@ -80,6 +80,11 @@ class HttpRateLimitFilterTest : public testing::Test { enable_x_ratelimit_headers: DRAFT_VERSION_03 )EOF"; + const std::string disable_x_envoy_ratelimited_header_config_ = R"EOF( + domain: foo + disable_x_envoy_ratelimited_header: true + )EOF"; + const std::string filter_config_ = R"EOF( domain: foo )EOF"; @@ -632,6 +637,44 @@ TEST_F(HttpRateLimitFilterTest, LimitResponseWithFilterHeaders) { filter_callbacks_.clusterInfo()->statsScope().counterFromStatName(upstream_rq_429_).value()); } +TEST_F(HttpRateLimitFilterTest, LimitResponseWithoutEnvoyRateLimitedHeader) { + SetUpTest(disable_x_envoy_ratelimited_header_config_); + InSequence s; + + EXPECT_CALL(route_rate_limit_, populateDescriptors(_, _, _, _, _, _)) + .WillOnce(SetArgReferee<1>(descriptor_)); + EXPECT_CALL(*client_, limit(_, _, _, _, _)) + .WillOnce( + WithArgs<0>(Invoke([&](Filters::Common::RateLimit::RequestCallbacks& callbacks) -> void { + request_callbacks_ = &callbacks; + }))); + + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter_->decodeHeaders(request_headers_, false)); + + Http::ResponseHeaderMapPtr h{new Http::TestResponseHeaderMapImpl()}; + Http::TestResponseHeaderMapImpl response_headers{{":status", "429"}}; + EXPECT_CALL(filter_callbacks_, encodeHeaders_(HeaderMapEqualRef(&response_headers), true)); + EXPECT_CALL(filter_callbacks_, continueDecoding()).Times(0); + EXPECT_CALL(filter_callbacks_.stream_info_, + setResponseFlag(StreamInfo::ResponseFlag::RateLimited)); + + request_callbacks_->complete(Filters::Common::RateLimit::LimitStatus::OverLimit, nullptr, + std::move(h), nullptr); + + EXPECT_EQ(1U, filter_callbacks_.clusterInfo() + ->statsScope() + .counterFromStatName(ratelimit_over_limit_) + .value()); + EXPECT_EQ( + 1U, + filter_callbacks_.clusterInfo()->statsScope().counterFromStatName(upstream_rq_4xx_).value()); + EXPECT_EQ( + 1U, + filter_callbacks_.clusterInfo()->statsScope().counterFromStatName(upstream_rq_429_).value()); + EXPECT_EQ("request_rate_limited", filter_callbacks_.details()); +} + TEST_F(HttpRateLimitFilterTest, LimitResponseRuntimeDisabled) { SetUpTest(filter_config_); InSequence s;