diff --git a/api/envoy/config/filter/fault/v2/fault.proto b/api/envoy/config/filter/fault/v2/fault.proto index 27f6de737a54..d0d12c07a64d 100644 --- a/api/envoy/config/filter/fault/v2/fault.proto +++ b/api/envoy/config/filter/fault/v2/fault.proto @@ -27,7 +27,7 @@ message FaultDelay { } // Fault delays are controlled via an HTTP header (if applicable). See the - // :ref:`http fault filter ` documentation for + // :ref:`HTTP fault filter ` documentation for // more information. message HeaderDelay { } @@ -65,7 +65,7 @@ message FaultRateLimit { } // Rate limits are controlled via an HTTP header (if applicable). See the - // :ref:`http fault filter ` documentation for + // :ref:`HTTP fault filter ` documentation for // more information. message HeaderLimit { } diff --git a/api/envoy/config/filter/http/fault/v2/fault.proto b/api/envoy/config/filter/http/fault/v2/fault.proto index 9465a45ebb1d..9ce49288076f 100644 --- a/api/envoy/config/filter/http/fault/v2/fault.proto +++ b/api/envoy/config/filter/http/fault/v2/fault.proto @@ -21,6 +21,12 @@ option (udpa.annotations.file_migrate).move_to_package = "envoy.extensions.filte // [#extension: envoy.filters.http.fault] message FaultAbort { + // Fault aborts are controlled via an HTTP header (if applicable). See the + // :ref:`HTTP fault filter ` documentation for + // more information. + message HeaderAbort { + } + reserved 1; oneof error_type { @@ -28,6 +34,9 @@ message FaultAbort { // HTTP status code to use to abort the HTTP request. uint32 http_status = 2 [(validate.rules).uint32 = {lt: 600 gte: 200}]; + + // Fault aborts are controlled via an HTTP header (if applicable). + HeaderAbort header_abort = 4; } // The percentage of requests/operations/connections that will be aborted with the error code diff --git a/api/envoy/extensions/filters/common/fault/v3/fault.proto b/api/envoy/extensions/filters/common/fault/v3/fault.proto index 14a88f30d8df..9976e17ce718 100644 --- a/api/envoy/extensions/filters/common/fault/v3/fault.proto +++ b/api/envoy/extensions/filters/common/fault/v3/fault.proto @@ -30,7 +30,7 @@ message FaultDelay { } // Fault delays are controlled via an HTTP header (if applicable). See the - // :ref:`http fault filter ` documentation for + // :ref:`HTTP fault filter ` documentation for // more information. message HeaderDelay { option (udpa.annotations.versioning).previous_message_type = @@ -75,7 +75,7 @@ message FaultRateLimit { } // Rate limits are controlled via an HTTP header (if applicable). See the - // :ref:`http fault filter ` documentation for + // :ref:`HTTP fault filter ` documentation for // more information. message HeaderLimit { option (udpa.annotations.versioning).previous_message_type = diff --git a/api/envoy/extensions/filters/http/fault/v3/fault.proto b/api/envoy/extensions/filters/http/fault/v3/fault.proto index 91ae0dceb6e9..6127ca848460 100644 --- a/api/envoy/extensions/filters/http/fault/v3/fault.proto +++ b/api/envoy/extensions/filters/http/fault/v3/fault.proto @@ -24,6 +24,14 @@ message FaultAbort { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.http.fault.v2.FaultAbort"; + // Fault aborts are controlled via an HTTP header (if applicable). See the + // :ref:`HTTP fault filter ` documentation for + // more information. + message HeaderAbort { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.http.fault.v2.FaultAbort.HeaderAbort"; + } + reserved 1; oneof error_type { @@ -31,6 +39,9 @@ message FaultAbort { // HTTP status code to use to abort the HTTP request. uint32 http_status = 2 [(validate.rules).uint32 = {lt: 600 gte: 200}]; + + // Fault aborts are controlled via an HTTP header (if applicable). + HeaderAbort header_abort = 4; } // The percentage of requests/operations/connections that will be aborted with the error code diff --git a/docs/root/configuration/http/http_filters/fault_filter.rst b/docs/root/configuration/http/http_filters/fault_filter.rst index 2a99312ab56a..928e1a3bef91 100644 --- a/docs/root/configuration/http/http_filters/fault_filter.rst +++ b/docs/root/configuration/http/http_filters/fault_filter.rst @@ -36,10 +36,18 @@ The fault filter has the capability to allow fault configuration to be specified This is useful in certain scenarios in which it is desired to allow the client to specify its own fault configuration. The currently supported header controls are: +* Request abort configuration via the *x-envoy-fault-abort-request* header. The header value + should be an integer that specifies the HTTP status code to return in response to a request + and must be in the range [200, 600). In order for the header to work, :ref:`header_abort + ` needs to be set. * Request delay configuration via the *x-envoy-fault-delay-request* header. The header value should be an integer that specifies the number of milliseconds to throttle the latency for. + In order for the header to work, :ref:`header_delay + ` needs to be set. * Response rate limit configuration via the *x-envoy-fault-throughput-response* header. The - header value should be an integer that specified the limit in KiB/s and must be > 0. + header value should be an integer that specified the limit in KiB/s and must be > 0. In order + for the header to work, :ref:`header_limit + ` needs to be set. .. attention:: @@ -57,6 +65,10 @@ options: typed_config: "@type": type.googleapis.com/envoy.config.filter.http.fault.v2.HTTPFault max_active_faults: 100 + abort: + header_abort: {} + percentage: + numerator: 100 delay: header_delay: {} percentage: diff --git a/docs/root/intro/version_history.rst b/docs/root/intro/version_history.rst index ab6d7df1698f..6a38480b032d 100644 --- a/docs/root/intro/version_history.rst +++ b/docs/root/intro/version_history.rst @@ -19,6 +19,7 @@ Version history of extension names is available in the :ref:`deprecated ` documentation. * ext_authz: disabled the use of lowercase string matcher for headers matching in HTTP-based `ext_authz`. Can be reverted temporarily by setting runtime feature `envoy.reloadable_features.ext_authz_http_service_enable_case_sensitive_string_matcher` to false. +* fault: added support for controlling abort faults with :ref:`HTTP header fault configuration ` to the HTTP fault filter. * http: added HTTP/1.1 flood protection. Can be temporarily disabled using the runtime feature `envoy.reloadable_features.http1_flood_protection` * http: fixing a bug in HTTP/1.0 responses where Connection: keep-alive was not appended for connections which were kept alive. * http: fixed a bug that could send extra METADATA frames and underflow memory when encoding METADATA frames on a connection that was dispatching data. diff --git a/generated_api_shadow/envoy/config/filter/fault/v2/fault.proto b/generated_api_shadow/envoy/config/filter/fault/v2/fault.proto index 27f6de737a54..d0d12c07a64d 100644 --- a/generated_api_shadow/envoy/config/filter/fault/v2/fault.proto +++ b/generated_api_shadow/envoy/config/filter/fault/v2/fault.proto @@ -27,7 +27,7 @@ message FaultDelay { } // Fault delays are controlled via an HTTP header (if applicable). See the - // :ref:`http fault filter ` documentation for + // :ref:`HTTP fault filter ` documentation for // more information. message HeaderDelay { } @@ -65,7 +65,7 @@ message FaultRateLimit { } // Rate limits are controlled via an HTTP header (if applicable). See the - // :ref:`http fault filter ` documentation for + // :ref:`HTTP fault filter ` documentation for // more information. message HeaderLimit { } diff --git a/generated_api_shadow/envoy/config/filter/http/fault/v2/fault.proto b/generated_api_shadow/envoy/config/filter/http/fault/v2/fault.proto index 9465a45ebb1d..9ce49288076f 100644 --- a/generated_api_shadow/envoy/config/filter/http/fault/v2/fault.proto +++ b/generated_api_shadow/envoy/config/filter/http/fault/v2/fault.proto @@ -21,6 +21,12 @@ option (udpa.annotations.file_migrate).move_to_package = "envoy.extensions.filte // [#extension: envoy.filters.http.fault] message FaultAbort { + // Fault aborts are controlled via an HTTP header (if applicable). See the + // :ref:`HTTP fault filter ` documentation for + // more information. + message HeaderAbort { + } + reserved 1; oneof error_type { @@ -28,6 +34,9 @@ message FaultAbort { // HTTP status code to use to abort the HTTP request. uint32 http_status = 2 [(validate.rules).uint32 = {lt: 600 gte: 200}]; + + // Fault aborts are controlled via an HTTP header (if applicable). + HeaderAbort header_abort = 4; } // The percentage of requests/operations/connections that will be aborted with the error code diff --git a/generated_api_shadow/envoy/extensions/filters/common/fault/v3/fault.proto b/generated_api_shadow/envoy/extensions/filters/common/fault/v3/fault.proto index 36f584c0c1ef..17230ebfacaa 100644 --- a/generated_api_shadow/envoy/extensions/filters/common/fault/v3/fault.proto +++ b/generated_api_shadow/envoy/extensions/filters/common/fault/v3/fault.proto @@ -30,7 +30,7 @@ message FaultDelay { } // Fault delays are controlled via an HTTP header (if applicable). See the - // :ref:`http fault filter ` documentation for + // :ref:`HTTP fault filter ` documentation for // more information. message HeaderDelay { option (udpa.annotations.versioning).previous_message_type = @@ -77,7 +77,7 @@ message FaultRateLimit { } // Rate limits are controlled via an HTTP header (if applicable). See the - // :ref:`http fault filter ` documentation for + // :ref:`HTTP fault filter ` documentation for // more information. message HeaderLimit { option (udpa.annotations.versioning).previous_message_type = diff --git a/generated_api_shadow/envoy/extensions/filters/http/fault/v3/fault.proto b/generated_api_shadow/envoy/extensions/filters/http/fault/v3/fault.proto index 91ae0dceb6e9..6127ca848460 100644 --- a/generated_api_shadow/envoy/extensions/filters/http/fault/v3/fault.proto +++ b/generated_api_shadow/envoy/extensions/filters/http/fault/v3/fault.proto @@ -24,6 +24,14 @@ message FaultAbort { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.http.fault.v2.FaultAbort"; + // Fault aborts are controlled via an HTTP header (if applicable). See the + // :ref:`HTTP fault filter ` documentation for + // more information. + message HeaderAbort { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.http.fault.v2.FaultAbort.HeaderAbort"; + } + reserved 1; oneof error_type { @@ -31,6 +39,9 @@ message FaultAbort { // HTTP status code to use to abort the HTTP request. uint32 http_status = 2 [(validate.rules).uint32 = {lt: 600 gte: 200}]; + + // Fault aborts are controlled via an HTTP header (if applicable). + HeaderAbort header_abort = 4; } // The percentage of requests/operations/connections that will be aborted with the error code diff --git a/source/extensions/filters/common/fault/BUILD b/source/extensions/filters/common/fault/BUILD index 09ee046e4857..e70a66db64eb 100644 --- a/source/extensions/filters/common/fault/BUILD +++ b/source/extensions/filters/common/fault/BUILD @@ -14,9 +14,11 @@ envoy_cc_library( hdrs = ["fault_config.h"], deps = [ "//include/envoy/http:header_map_interface", + "//source/common/http:codes_lib", "//source/common/http:headers_lib", "//source/common/protobuf:utility_lib", "@envoy_api//envoy/extensions/filters/common/fault/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/filters/http/fault/v3:pkg_cc_proto", "@envoy_api//envoy/type/v3:pkg_cc_proto", ], ) diff --git a/source/extensions/filters/common/fault/fault_config.cc b/source/extensions/filters/common/fault/fault_config.cc index efc7b07faf61..2dc708044d6d 100644 --- a/source/extensions/filters/common/fault/fault_config.cc +++ b/source/extensions/filters/common/fault/fault_config.cc @@ -1,6 +1,7 @@ #include "extensions/filters/common/fault/fault_config.h" #include "envoy/extensions/filters/common/fault/v3/fault.pb.h" +#include "envoy/extensions/filters/http/fault/v3/fault.pb.h" #include "common/protobuf/utility.h" @@ -10,6 +11,40 @@ namespace Filters { namespace Common { namespace Fault { +FaultAbortConfig::FaultAbortConfig( + const envoy::extensions::filters::http::fault::v3::FaultAbort& abort_config) + : percentage_(abort_config.percentage()) { + switch (abort_config.error_type_case()) { + case envoy::extensions::filters::http::fault::v3::FaultAbort::ErrorTypeCase::kHttpStatus: + provider_ = std::make_unique(abort_config.http_status()); + break; + case envoy::extensions::filters::http::fault::v3::FaultAbort::ErrorTypeCase::kHeaderAbort: + provider_ = std::make_unique(); + break; + case envoy::extensions::filters::http::fault::v3::FaultAbort::ErrorTypeCase::ERROR_TYPE_NOT_SET: + NOT_REACHED_GCOVR_EXCL_LINE; + } +} + +absl::optional +FaultAbortConfig::HeaderAbortProvider::statusCode(const Http::HeaderEntry* header) const { + absl::optional ret; + if (header == nullptr) { + return ret; + } + + uint64_t code; + if (!absl::SimpleAtoi(header->value().getStringView(), &code)) { + return ret; + } + + if (code >= 200 && code < 600) { + ret = static_cast(code); + } + + return ret; +} + FaultDelayConfig::FaultDelayConfig( const envoy::extensions::filters::common::fault::v3::FaultDelay& delay_config) : percentage_(delay_config.percentage()) { diff --git a/source/extensions/filters/common/fault/fault_config.h b/source/extensions/filters/common/fault/fault_config.h index 41e0ee71d916..985fae12e0d1 100644 --- a/source/extensions/filters/common/fault/fault_config.h +++ b/source/extensions/filters/common/fault/fault_config.h @@ -1,9 +1,11 @@ #pragma once #include "envoy/extensions/filters/common/fault/v3/fault.pb.h" +#include "envoy/extensions/filters/http/fault/v3/fault.pb.h" #include "envoy/http/header_map.h" #include "envoy/type/v3/percent.pb.h" +#include "common/http/codes.h" #include "common/http/headers.h" #include "common/singleton/const_singleton.h" @@ -20,10 +22,60 @@ class HeaderNameValues { const Http::LowerCaseString DelayRequest{absl::StrCat(prefix(), "-fault-delay-request")}; const Http::LowerCaseString ThroughputResponse{ absl::StrCat(prefix(), "-fault-throughput-response")}; + const Http::LowerCaseString AbortRequest{absl::StrCat(prefix(), "-fault-abort-request")}; }; using HeaderNames = ConstSingleton; +class FaultAbortConfig { +public: + FaultAbortConfig(const envoy::extensions::filters::http::fault::v3::FaultAbort& abort_config); + + const envoy::type::v3::FractionalPercent& percentage() const { return percentage_; } + absl::optional statusCode(const Http::HeaderEntry* header) const { + return provider_->statusCode(header); + } + +private: + // Abstract abort provider. + class AbortProvider { + public: + virtual ~AbortProvider() = default; + + // Return the HTTP status code to use. Optionally passed an HTTP header that may contain the + // HTTP status code depending on the provider implementation. + virtual absl::optional statusCode(const Http::HeaderEntry* header) const PURE; + }; + + // Delay provider that uses a fixed abort status code. + class FixedAbortProvider : public AbortProvider { + public: + FixedAbortProvider(uint64_t status_code) : status_code_(status_code) {} + + // AbortProvider + absl::optional statusCode(const Http::HeaderEntry*) const override { + return static_cast(status_code_); + } + + private: + const uint64_t status_code_; + }; + + // Abort provider the reads a status code from an HTTP header. + class HeaderAbortProvider : public AbortProvider { + public: + // AbortProvider + absl::optional statusCode(const Http::HeaderEntry* header) const override; + }; + + using AbortProviderPtr = std::unique_ptr; + + AbortProviderPtr provider_; + const envoy::type::v3::FractionalPercent percentage_; +}; + +using FaultAbortConfigPtr = std::unique_ptr; + /** * Generic configuration for a delay fault. */ diff --git a/source/extensions/filters/http/fault/fault_filter.cc b/source/extensions/filters/http/fault/fault_filter.cc index dc2cdd7364e3..f396f5a12ff6 100644 --- a/source/extensions/filters/http/fault/fault_filter.cc +++ b/source/extensions/filters/http/fault/fault_filter.cc @@ -49,9 +49,8 @@ FaultSettings::FaultSettings(const envoy::extensions::filters::http::fault::v3:: PROTOBUF_GET_STRING_OR_DEFAULT(fault, response_rate_limit_percent_runtime, RuntimeKeys::get().ResponseRateLimitPercentKey)) { if (fault.has_abort()) { - const auto& abort = fault.abort(); - abort_percentage_ = abort.percentage(); - http_status_ = abort.http_status(); + request_abort_config_ = + std::make_unique(fault.abort()); } if (fault.has_delay()) { @@ -156,8 +155,8 @@ Http::FilterHeadersStatus FaultFilter::decodeHeaders(Http::RequestHeaderMap& hea absl::optional duration = delayDuration(headers); if (duration.has_value()) { - delay_timer_ = - decoder_callbacks_->dispatcher().createTimer([this]() -> void { postDelayInjection(); }); + delay_timer_ = decoder_callbacks_->dispatcher().createTimer( + [this, &headers]() -> void { postDelayInjection(headers); }); ENVOY_LOG(debug, "fault: delaying request {}ms", duration.value().count()); delay_timer_->enableTimer(duration.value(), &decoder_callbacks_->scope()); recordDelaysInjectedStats(); @@ -165,8 +164,9 @@ Http::FilterHeadersStatus FaultFilter::decodeHeaders(Http::RequestHeaderMap& hea return Http::FilterHeadersStatus::StopIteration; } - if (isAbortEnabled()) { - abortWithHTTPStatus(); + const auto abort_code = abortHttpStatus(headers); + if (abort_code.has_value()) { + abortWithHTTPStatus(abort_code.value()); return Http::FilterHeadersStatus::StopIteration; } @@ -222,25 +222,31 @@ bool FaultFilter::faultOverflow() { } bool FaultFilter::isDelayEnabled() { - if (fault_settings_->requestDelay() == nullptr) { + const auto request_delay = fault_settings_->requestDelay(); + if (request_delay == nullptr) { return false; } if (!downstream_cluster_delay_percent_key_.empty()) { - return config_->runtime().snapshot().featureEnabled( - downstream_cluster_delay_percent_key_, fault_settings_->requestDelay()->percentage()); + return config_->runtime().snapshot().featureEnabled(downstream_cluster_delay_percent_key_, + request_delay->percentage()); } - return config_->runtime().snapshot().featureEnabled( - fault_settings_->delayPercentRuntime(), fault_settings_->requestDelay()->percentage()); + return config_->runtime().snapshot().featureEnabled(fault_settings_->delayPercentRuntime(), + request_delay->percentage()); } bool FaultFilter::isAbortEnabled() { + const auto request_abort = fault_settings_->requestAbort(); + if (request_abort == nullptr) { + return false; + } + if (!downstream_cluster_abort_percent_key_.empty()) { return config_->runtime().snapshot().featureEnabled(downstream_cluster_abort_percent_key_, - fault_settings_->abortPercentage()); + request_abort->percentage()); } return config_->runtime().snapshot().featureEnabled(fault_settings_->abortPercentRuntime(), - fault_settings_->abortPercentage()); + request_abort->percentage()); } absl::optional @@ -275,17 +281,30 @@ FaultFilter::delayDuration(const Http::RequestHeaderMap& request_headers) { return ret; } -uint64_t FaultFilter::abortHttpStatus() { - // TODO(mattklein123): check http status codes obtained from runtime. - uint64_t http_status = config_->runtime().snapshot().getInteger( - fault_settings_->abortHttpStatusRuntime(), fault_settings_->abortCode()); +absl::optional +FaultFilter::abortHttpStatus(const Http::RequestHeaderMap& request_headers) { + if (!isAbortEnabled()) { + return absl::nullopt; + } + + // See if the configured abort provider has a default status code, if not there is no abort status + // code (e.g., header configuration and no/invalid header). + const auto config_abort = fault_settings_->requestAbort()->statusCode( + request_headers.get(Filters::Common::Fault::HeaderNames::get().AbortRequest)); + if (!config_abort.has_value()) { + return absl::nullopt; + } + + auto status_code = static_cast(config_abort.value()); + auto code = static_cast(config_->runtime().snapshot().getInteger( + fault_settings_->abortHttpStatusRuntime(), status_code)); if (!downstream_cluster_abort_http_status_key_.empty()) { - http_status = config_->runtime().snapshot().getInteger( - downstream_cluster_abort_http_status_key_, http_status); + code = static_cast(config_->runtime().snapshot().getInteger( + downstream_cluster_abort_http_status_key_, status_code)); } - return http_status; + return code; } void FaultFilter::recordDelaysInjectedStats() { @@ -350,22 +369,22 @@ void FaultFilter::onDestroy() { } } -void FaultFilter::postDelayInjection() { +void FaultFilter::postDelayInjection(const Http::RequestHeaderMap& request_headers) { resetTimerState(); // Delays can be followed by aborts - if (isAbortEnabled()) { - abortWithHTTPStatus(); + const auto abort_code = abortHttpStatus(request_headers); + if (abort_code.has_value()) { + abortWithHTTPStatus(abort_code.value()); } else { // Continue request processing. decoder_callbacks_->continueDecoding(); } } -void FaultFilter::abortWithHTTPStatus() { +void FaultFilter::abortWithHTTPStatus(Http::Code abort_code) { decoder_callbacks_->streamInfo().setResponseFlag(StreamInfo::ResponseFlag::FaultInjected); - decoder_callbacks_->sendLocalReply(static_cast(abortHttpStatus()), - "fault filter abort", nullptr, absl::nullopt, + decoder_callbacks_->sendLocalReply(abort_code, "fault filter abort", nullptr, absl::nullopt, RcDetails::get().FaultAbort); recordAbortsInjectedStats(); } diff --git a/source/extensions/filters/http/fault/fault_filter.h b/source/extensions/filters/http/fault/fault_filter.h index 40ebfadaa31f..9572909a882f 100644 --- a/source/extensions/filters/http/fault/fault_filter.h +++ b/source/extensions/filters/http/fault/fault_filter.h @@ -52,8 +52,9 @@ class FaultSettings : public Router::RouteSpecificFilterConfig { const std::vector& filterHeaders() const { return fault_filter_headers_; } - envoy::type::v3::FractionalPercent abortPercentage() const { return abort_percentage_; } - uint64_t abortCode() const { return http_status_; } + const Filters::Common::Fault::FaultAbortConfig* requestAbort() const { + return request_abort_config_.get(); + } const Filters::Common::Fault::FaultDelayConfig* requestDelay() const { return request_delay_config_.get(); } @@ -86,8 +87,8 @@ class FaultSettings : public Router::RouteSpecificFilterConfig { using RuntimeKeys = ConstSingleton; envoy::type::v3::FractionalPercent abort_percentage_; - uint64_t http_status_{}; // HTTP or gRPC return codes Filters::Common::Fault::FaultDelayConfigPtr request_delay_config_; + Filters::Common::Fault::FaultAbortConfigPtr request_abort_config_; std::string upstream_cluster_; // restrict faults to specific upstream cluster const std::vector fault_filter_headers_; absl::flat_hash_set downstream_nodes_{}; // Inject failures for specific downstream @@ -243,15 +244,15 @@ class FaultFilter : public Http::StreamFilter, Logger::Loggable delayDuration(const Http::RequestHeaderMap& request_headers); - uint64_t abortHttpStatus(); + absl::optional abortHttpStatus(const Http::RequestHeaderMap& request_headers); void maybeIncActiveFaults(); void maybeSetupResponseRateLimit(const Http::RequestHeaderMap& request_headers); diff --git a/test/extensions/filters/common/fault/fault_config_test.cc b/test/extensions/filters/common/fault/fault_config_test.cc index a62ee5a81ccf..a402e1bf6156 100644 --- a/test/extensions/filters/common/fault/fault_config_test.cc +++ b/test/extensions/filters/common/fault/fault_config_test.cc @@ -13,6 +13,33 @@ namespace Common { namespace Fault { namespace { +TEST(FaultConfigTest, FaultAbortHeaderConfig) { + envoy::extensions::filters::http::fault::v3::FaultAbort proto_config; + proto_config.mutable_header_abort(); + FaultAbortConfig config(proto_config); + + // No header. + EXPECT_EQ(absl::nullopt, config.statusCode(nullptr)); + + // Header with bad data. + Http::TestHeaderMapImpl bad_headers{{"x-envoy-fault-abort-request", "abc"}}; + EXPECT_EQ(absl::nullopt, config.statusCode(bad_headers.get(HeaderNames::get().AbortRequest))); + + // Out of range header - value too low. + Http::TestHeaderMapImpl too_low_headers{{"x-envoy-fault-abort-request", "199"}}; + EXPECT_EQ(absl::nullopt, config.statusCode(too_low_headers.get(HeaderNames::get().AbortRequest))); + + // Out of range header - value too high. + Http::TestHeaderMapImpl too_high_headers{{"x-envoy-fault-abort-request", "600"}}; + EXPECT_EQ(absl::nullopt, + config.statusCode(too_high_headers.get(HeaderNames::get().AbortRequest))); + + // Valid header. + Http::TestHeaderMapImpl good_headers{{"x-envoy-fault-abort-request", "401"}}; + EXPECT_EQ(Http::Code::Unauthorized, + config.statusCode(good_headers.get(HeaderNames::get().AbortRequest)).value()); +} + TEST(FaultConfigTest, FaultDelayHeaderConfig) { envoy::extensions::filters::common::fault::v3::FaultDelay proto_config; proto_config.mutable_header_delay(); diff --git a/test/extensions/filters/http/fault/fault_filter_integration_test.cc b/test/extensions/filters/http/fault/fault_filter_integration_test.cc index 46b7e23ef27f..44da53d1ec0c 100644 --- a/test/extensions/filters/http/fault/fault_filter_integration_test.cc +++ b/test/extensions/filters/http/fault/fault_filter_integration_test.cc @@ -33,6 +33,10 @@ name: fault name: fault typed_config: "@type": type.googleapis.com/envoy.config.filter.http.fault.v2.HTTPFault + abort: + header_abort: {} + percentage: + numerator: 100 delay: header_delay: {} percentage: @@ -65,6 +69,7 @@ name: fault auto response = sendRequestAndWaitForResponse(default_request_headers_, 0, default_response_headers_, 1024); + EXPECT_EQ(0UL, test_server_->counter("http.config_test.fault.aborts_injected")->value()); EXPECT_EQ(0UL, test_server_->counter("http.config_test.fault.delays_injected")->value()); EXPECT_EQ(0UL, test_server_->counter("http.config_test.fault.response_rl_injected")->value()); } @@ -88,6 +93,7 @@ TEST_P(FaultIntegrationTestAllProtocols, ResponseRateLimitNoTrailers) { decoder->waitForBodyData(127); decoder->waitForEndStream(); + EXPECT_EQ(0UL, test_server_->counter("http.config_test.fault.aborts_injected")->value()); EXPECT_EQ(0UL, test_server_->counter("http.config_test.fault.delays_injected")->value()); EXPECT_EQ(1UL, test_server_->counter("http.config_test.fault.response_rl_injected")->value()); } @@ -122,10 +128,32 @@ TEST_P(FaultIntegrationTestAllProtocols, HeaderFaultConfig) { decoder->waitForBodyData(128); decoder->waitForEndStream(); + EXPECT_EQ(0UL, test_server_->counter("http.config_test.fault.aborts_injected")->value()); EXPECT_EQ(1UL, test_server_->counter("http.config_test.fault.delays_injected")->value()); EXPECT_EQ(1UL, test_server_->counter("http.config_test.fault.response_rl_injected")->value()); } +// Request abort controlled via header configuration. +TEST_P(FaultIntegrationTestAllProtocols, HeaderFaultAbortConfig) { + initializeFilter(header_fault_config_); + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + + auto response = codec_client_->makeHeaderOnlyRequest( + Http::TestRequestHeaderMapImpl{{":method", "GET"}, + {":path", "/test/long/url"}, + {":scheme", "http"}, + {":authority", "host"}, + {"x-envoy-fault-abort-request", "429"}}); + response->waitForEndStream(); + + EXPECT_TRUE(response->complete()); + EXPECT_THAT(response->headers(), Envoy::Http::HttpStatusIs("429")); + + EXPECT_EQ(1UL, test_server_->counter("http.config_test.fault.aborts_injected")->value()); + EXPECT_EQ(0UL, test_server_->counter("http.config_test.fault.delays_injected")->value()); + EXPECT_EQ(0UL, test_server_->counter("http.config_test.fault.response_rl_injected")->value()); +} + // Header configuration with no headers, so no fault injection. TEST_P(FaultIntegrationTestAllProtocols, HeaderFaultConfigNoHeaders) { initializeFilter(header_fault_config_); @@ -133,6 +161,7 @@ TEST_P(FaultIntegrationTestAllProtocols, HeaderFaultConfigNoHeaders) { auto response = sendRequestAndWaitForResponse(default_request_headers_, 0, default_response_headers_, 1024); + EXPECT_EQ(0UL, test_server_->counter("http.config_test.fault.aborts_injected")->value()); EXPECT_EQ(0UL, test_server_->counter("http.config_test.fault.delays_injected")->value()); EXPECT_EQ(0UL, test_server_->counter("http.config_test.fault.response_rl_injected")->value()); } @@ -168,6 +197,7 @@ TEST_P(FaultIntegrationTestHttp2, ResponseRateLimitTrailersBodyFlushed) { decoder->waitForEndStream(); EXPECT_NE(nullptr, decoder->trailers()); + EXPECT_EQ(0UL, test_server_->counter("http.config_test.fault.aborts_injected")->value()); EXPECT_EQ(0UL, test_server_->counter("http.config_test.fault.delays_injected")->value()); EXPECT_EQ(1UL, test_server_->counter("http.config_test.fault.response_rl_injected")->value()); } @@ -194,6 +224,7 @@ TEST_P(FaultIntegrationTestHttp2, ResponseRateLimitTrailersBodyNotFlushed) { decoder->waitForEndStream(); EXPECT_NE(nullptr, decoder->trailers()); + EXPECT_EQ(0UL, test_server_->counter("http.config_test.fault.aborts_injected")->value()); EXPECT_EQ(0UL, test_server_->counter("http.config_test.fault.delays_injected")->value()); EXPECT_EQ(1UL, test_server_->counter("http.config_test.fault.response_rl_injected")->value()); } diff --git a/test/extensions/filters/http/fault/fault_filter_test.cc b/test/extensions/filters/http/fault/fault_filter_test.cc index 8d2f25904de7..eb83202d83c8 100644 --- a/test/extensions/filters/http/fault/fault_filter_test.cc +++ b/test/extensions/filters/http/fault/fault_filter_test.cc @@ -90,6 +90,13 @@ class FaultFilterTest : public testing::Test { http_status: 503 )EOF"; + const std::string header_abort_only_yaml = R"EOF( + abort: + header_abort: {} + percentage: + numerator: 100 + )EOF"; + const std::string fixed_delay_and_abort_match_headers_yaml = R"EOF( delay: type: fixed @@ -272,6 +279,53 @@ TEST_F(FaultFilterTest, AbortWithHttpStatus) { EXPECT_EQ("fault_filter_abort", decoder_filter_callbacks_.details_); } +TEST_F(FaultFilterTest, HeaderAbortWithHttpStatus) { + SetUpTest(header_abort_only_yaml); + + request_headers_.addCopy("x-envoy-fault-abort-request", "429"); + + EXPECT_CALL(runtime_.snapshot_, + getInteger("fault.http.max_active_faults", std::numeric_limits::max())) + .WillOnce(Return(std::numeric_limits::max())); + + EXPECT_CALL(decoder_filter_callbacks_, continueDecoding()).Times(0); + EXPECT_CALL(decoder_filter_callbacks_.stream_info_, + setResponseFlag(StreamInfo::ResponseFlag::DelayInjected)) + .Times(0); + + // Abort related calls + EXPECT_CALL(runtime_.snapshot_, + featureEnabled("fault.http.abort.abort_percent", + Matcher(Percent(100)))) + .WillOnce(Return(true)); + + EXPECT_CALL(runtime_.snapshot_, getInteger("fault.http.abort.http_status", 429)) + .WillOnce(Return(429)); + + Http::TestResponseHeaderMapImpl response_headers{ + {":status", "429"}, {"content-length", "18"}, {"content-type", "text/plain"}}; + EXPECT_CALL(decoder_filter_callbacks_, + encodeHeaders_(HeaderMapEqualRef(&response_headers), false)); + EXPECT_CALL(decoder_filter_callbacks_, encodeData(_, true)); + + EXPECT_CALL(decoder_filter_callbacks_.stream_info_, + setResponseFlag(StreamInfo::ResponseFlag::FaultInjected)); + + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter_->decodeHeaders(request_headers_, false)); + Http::MetadataMap metadata_map{{"metadata", "metadata"}}; + EXPECT_EQ(Http::FilterMetadataStatus::Continue, filter_->decodeMetadata(metadata_map)); + EXPECT_EQ(1UL, config_->stats().active_faults_.value()); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(data_, false)); + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers_)); + filter_->onDestroy(); + + EXPECT_EQ(0UL, config_->stats().delays_injected_.value()); + EXPECT_EQ(1UL, config_->stats().aborts_injected_.value()); + EXPECT_EQ(0UL, config_->stats().active_faults_.value()); + EXPECT_EQ("fault_filter_abort", decoder_filter_callbacks_.details_); +} + TEST_F(FaultFilterTest, FixedDelayZeroDuration) { SetUpTest(fixed_delay_only_yaml); @@ -289,12 +343,6 @@ TEST_F(FaultFilterTest, FixedDelayZeroDuration) { EXPECT_CALL(runtime_.snapshot_, getInteger("fault.http.delay.fixed_duration_ms", 5000)) .WillOnce(Return(0)); - // Abort related calls - EXPECT_CALL(runtime_.snapshot_, - featureEnabled("fault.http.abort.abort_percent", - Matcher(Percent(0)))) - .WillOnce(Return(false)); - EXPECT_CALL(runtime_.snapshot_, getInteger("fault.http.abort.http_status", _)).Times(0); EXPECT_CALL(decoder_filter_callbacks_, encodeHeaders_(_, _)).Times(0); EXPECT_CALL(decoder_filter_callbacks_.stream_info_, setResponseFlag(_)).Times(0); @@ -351,12 +399,6 @@ TEST_F(FaultFilterTest, FixedDelayDeprecatedPercentAndNonZeroDuration) { EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, filter_->decodeHeaders(request_headers_, false)); - // Abort related calls - EXPECT_CALL(runtime_.snapshot_, - featureEnabled("fault.http.abort.abort_percent", - Matcher(Percent(0)))) - .WillOnce(Return(false)); - // Delay only case EXPECT_CALL(runtime_.snapshot_, getInteger("fault.http.abort.http_status", _)).Times(0); EXPECT_CALL(decoder_filter_callbacks_, encodeHeaders_(_, _)).Times(0); @@ -401,12 +443,6 @@ TEST_F(FaultFilterTest, DelayForDownstreamCluster) { EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, filter_->decodeHeaders(request_headers_, false)); - // Abort related calls. - EXPECT_CALL(runtime_.snapshot_, - featureEnabled("fault.http.cluster.abort.abort_percent", - Matcher(Percent(0)))) - .WillOnce(Return(false)); - // Delay only case, no aborts. EXPECT_CALL(runtime_.snapshot_, getInteger("fault.http.cluster.abort.http_status", _)).Times(0); EXPECT_CALL(runtime_.snapshot_, getInteger("fault.http.abort.http_status", _)).Times(0); @@ -773,12 +809,6 @@ TEST_F(FaultFilterTest, FaultWithTargetClusterMatchSuccess) { EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, filter_->decodeHeaders(request_headers_, false)); - // Abort related calls - EXPECT_CALL(runtime_.snapshot_, - featureEnabled("fault.http.abort.abort_percent", - Matcher(Percent(0)))) - .WillOnce(Return(false)); - // Delay only case EXPECT_CALL(runtime_.snapshot_, getInteger("fault.http.abort.http_status", _)).Times(0); EXPECT_CALL(decoder_filter_callbacks_, encodeHeaders_(_, _)).Times(0); @@ -893,12 +923,6 @@ void FaultFilterTest::TestPerFilterConfigFault( EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, filter_->decodeHeaders(request_headers_, false)); - // Abort related calls - EXPECT_CALL(runtime_.snapshot_, - featureEnabled("fault.http.abort.abort_percent", - Matcher(Percent(0)))) - .WillOnce(Return(false)); - EXPECT_CALL(decoder_filter_callbacks_, continueDecoding()); timer_->invokeCallback(); @@ -949,10 +973,6 @@ class FaultFilterRateLimitTest : public FaultFilterTest { featureEnabled("fault.http.rate_limit.response_percent", Matcher(Percent(100)))) .WillOnce(Return(enable_runtime)); - EXPECT_CALL(runtime_.snapshot_, - featureEnabled("fault.http.abort.abort_percent", - Matcher(Percent(0)))) - .WillOnce(Return(false)); } };