From 2fcdcf4b572de4908423b60022359fab7c4553b2 Mon Sep 17 00:00:00 2001 From: code Date: Fri, 20 Dec 2024 05:11:43 +0800 Subject: [PATCH] header mutation: add query parameter mutation support (#37555) This added query parameter mutation support for header mutation filter. Risk Level: low. Testing: unit. Docs Changes: n/a. Release Notes: added. Platform Specific Features: n/a. --------- Signed-off-by: wangbaiping(wbpcode) Signed-off-by: code Co-authored-by: Adi (Suissa) Peleg --- api/envoy/config/core/v3/base.proto | 41 +- .../filters/http/header_mutation/v3/BUILD | 1 + .../header_mutation/v3/header_mutation.proto | 5 + changelogs/current.yaml | 6 + .../filters/http/header_mutation/config.cc | 10 +- .../http/header_mutation/header_mutation.cc | 178 ++++++-- .../http/header_mutation/header_mutation.h | 81 +++- .../http/header_mutation/config_test.cc | 141 +++++- .../header_mutation/header_mutation_test.cc | 432 +++++++++++++----- tools/code_format/config.yaml | 1 - 10 files changed, 698 insertions(+), 198 deletions(-) diff --git a/api/envoy/config/core/v3/base.proto b/api/envoy/config/core/v3/base.proto index 57f59373395f..48ff5e7eee1e 100644 --- a/api/envoy/config/core/v3/base.proto +++ b/api/envoy/config/core/v3/base.proto @@ -303,12 +303,31 @@ message RuntimeFeatureFlag { string runtime_key = 2 [(validate.rules).string = {min_len: 1}]; } +// Please use :ref:`KeyValuePair ` instead. +// [#not-implemented-hide:] message KeyValue { + // The key of the key/value pair. + string key = 1 [ + deprecated = true, + (validate.rules).string = {min_len: 1 max_bytes: 16384}, + (envoy.annotations.deprecated_at_minor_version) = "3.0" + ]; + + // The value of the key/value pair. + // + // The ``bytes`` type is used. This means if JSON or YAML is used to to represent the + // configuration, the value must be base64 encoded. This is unfriendly for users in most + // use scenarios of this message. + // + bytes value = 2 [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; +} + +message KeyValuePair { // The key of the key/value pair. string key = 1 [(validate.rules).string = {min_len: 1 max_bytes: 16384}]; // The value of the key/value pair. - bytes value = 2; + google.protobuf.Value value = 2; } // Key/value pair plus option to control append behavior. This is used to specify @@ -339,8 +358,18 @@ message KeyValueAppend { OVERWRITE_IF_EXISTS = 3; } - // Key/value pair entry that this option to append or overwrite. - KeyValue entry = 1 [(validate.rules).message = {required: true}]; + // The single key/value pair record to be appended or overridden. This field must be set. + KeyValuePair record = 3; + + // Key/value pair entry that this option to append or overwrite. This field is deprecated + // and please use :ref:`record ` + // as replacement. + // [#not-implemented-hide:] + KeyValue entry = 1 [ + deprecated = true, + (validate.rules).message = {skip: true}, + (envoy.annotations.deprecated_at_minor_version) = "3.0" + ]; // Describes the action taken to append/overwrite the given value for an existing // key or to only add this key if it's absent. @@ -349,10 +378,12 @@ message KeyValueAppend { // Key/value pair to append or remove. message KeyValueMutation { - // Key/value pair to append or overwrite. Only one of ``append`` or ``remove`` can be set. + // Key/value pair to append or overwrite. Only one of ``append`` or ``remove`` can be set or + // the configuration will be rejected. KeyValueAppend append = 1; - // Key to remove. Only one of ``append`` or ``remove`` can be set. + // Key to remove. Only one of ``append`` or ``remove`` can be set or the configuration will be + // rejected. string remove = 2 [(validate.rules).string = {max_bytes: 16384}]; } diff --git a/api/envoy/extensions/filters/http/header_mutation/v3/BUILD b/api/envoy/extensions/filters/http/header_mutation/v3/BUILD index 876a007c83cf..412f3ebb8dde 100644 --- a/api/envoy/extensions/filters/http/header_mutation/v3/BUILD +++ b/api/envoy/extensions/filters/http/header_mutation/v3/BUILD @@ -7,6 +7,7 @@ licenses(["notice"]) # Apache 2 api_proto_package( deps = [ "//envoy/config/common/mutation_rules/v3:pkg", + "//envoy/config/core/v3:pkg", "@com_github_cncf_xds//udpa/annotations:pkg", ], ) diff --git a/api/envoy/extensions/filters/http/header_mutation/v3/header_mutation.proto b/api/envoy/extensions/filters/http/header_mutation/v3/header_mutation.proto index db267d2c591f..ca951db82325 100644 --- a/api/envoy/extensions/filters/http/header_mutation/v3/header_mutation.proto +++ b/api/envoy/extensions/filters/http/header_mutation/v3/header_mutation.proto @@ -3,6 +3,7 @@ syntax = "proto3"; package envoy.extensions.filters.http.header_mutation.v3; import "envoy/config/common/mutation_rules/v3/mutation_rules.proto"; +import "envoy/config/core/v3/base.proto"; import "udpa/annotations/status.proto"; @@ -19,6 +20,10 @@ message Mutations { // The request mutations are applied before the request is forwarded to the upstream cluster. repeated config.common.mutation_rules.v3.HeaderMutation request_mutations = 1; + // The ``path`` header query parameter mutations are applied after ``request_mutations`` and before the request + // is forwarded to the next filter in the filter chain. + repeated config.core.v3.KeyValueMutation query_parameter_mutations = 3; + // The response mutations are applied before the response is sent to the downstream client. repeated config.common.mutation_rules.v3.HeaderMutation response_mutations = 2; } diff --git a/changelogs/current.yaml b/changelogs/current.yaml index 73abf706e60e..419c04e57b16 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -364,6 +364,12 @@ new_features: change: | Added new health check filter stats including total requests, successful/failed checks, cached responses, and cluster health status counters. These stats help track health check behavior and cluster health state. +- area: http + change: | + Add :ref:`query parameter mutations + ` + to :ref:`Header Mutation Filter ` + for adding/removing query parameters on a request. - area: local_ratelimit change: | Added per descriptor custom hits addend support for local rate limit filter. See :ref:`hits_addend diff --git a/source/extensions/filters/http/header_mutation/config.cc b/source/extensions/filters/http/header_mutation/config.cc index ab62efc858f3..096b3870c45a 100644 --- a/source/extensions/filters/http/header_mutation/config.cc +++ b/source/extensions/filters/http/header_mutation/config.cc @@ -13,7 +13,10 @@ absl::StatusOr HeaderMutationFactoryConfig::createFilterFactoryFromProtoTyped( const ProtoConfig& config, const std::string&, DualInfo, Server::Configuration::ServerFactoryContext&) { - auto filter_config = std::make_shared(config); + absl::Status creation_status = absl::OkStatus(); + auto filter_config = std::make_shared(config, creation_status); + RETURN_IF_NOT_OK_REF(creation_status); + return [filter_config](Http::FilterChainFactoryCallbacks& callbacks) -> void { callbacks.addStreamFilter(std::make_shared(filter_config)); }; @@ -23,7 +26,10 @@ absl::StatusOr HeaderMutationFactoryConfig::createRouteSpecificFilterConfigTyped( const PerRouteProtoConfig& proto_config, Server::Configuration::ServerFactoryContext&, ProtobufMessage::ValidationVisitor&) { - return std::make_shared(proto_config); + absl::Status creation_status = absl::OkStatus(); + auto route_config = std::make_shared(proto_config, creation_status); + RETURN_IF_NOT_OK_REF(creation_status); + return route_config; } using UpstreamHeaderMutationFactoryConfig = HeaderMutationFactoryConfig; diff --git a/source/extensions/filters/http/header_mutation/header_mutation.cc b/source/extensions/filters/http/header_mutation/header_mutation.cc index 259f180d1765..f2955f2b86b2 100644 --- a/source/extensions/filters/http/header_mutation/header_mutation.cc +++ b/source/extensions/filters/http/header_mutation/header_mutation.cc @@ -12,70 +12,164 @@ namespace Extensions { namespace HttpFilters { namespace HeaderMutation { -void Mutations::mutateRequestHeaders(Http::HeaderMap& headers, - const Formatter::HttpFormatterContext& ctx, +void QueryParameterMutationAppend::mutateQueryParameter( + Http::Utility::QueryParamsMulti& params, const Formatter::HttpFormatterContext& context, + const StreamInfo::StreamInfo& stream_info) const { + + switch (action_) { + PANIC_ON_PROTO_ENUM_SENTINEL_VALUES; + case ParameterAppendProto::APPEND_IF_EXISTS_OR_ADD: + params.add(key_, formatter_->formatWithContext(context, stream_info)); + return; + case ParameterAppendProto::ADD_IF_ABSENT: { + auto iter = params.data().find(key_); + if (iter == params.data().end()) { + params.add(key_, formatter_->formatWithContext(context, stream_info)); + } + break; + } + case ParameterAppendProto::OVERWRITE_IF_EXISTS: { + auto iter = params.data().find(key_); + if (iter != params.data().end()) { + params.overwrite(key_, formatter_->formatWithContext(context, stream_info)); + } + break; + } + case ParameterAppendProto::OVERWRITE_IF_EXISTS_OR_ADD: + params.overwrite(key_, formatter_->formatWithContext(context, stream_info)); + break; + } +} + +Mutations::Mutations(const MutationsProto& config, absl::Status& creation_status) { + auto request_mutations_or_error = HeaderMutations::create(config.request_mutations()); + SET_AND_RETURN_IF_NOT_OK(request_mutations_or_error.status(), creation_status); + request_mutations_ = std::move(request_mutations_or_error.value()); + + auto response_mutations_or_error = HeaderMutations::create(config.response_mutations()); + SET_AND_RETURN_IF_NOT_OK(response_mutations_or_error.status(), creation_status); + response_mutations_ = std::move(response_mutations_or_error.value()); + + query_query_parameter_mutations_.reserve(config.query_parameter_mutations_size()); + for (const auto& mutation : config.query_parameter_mutations()) { + if (mutation.has_append()) { + if (!mutation.remove().empty()) { + creation_status = + absl::InvalidArgumentError("Only one of 'append'/'remove can be specified."); + return; + } + + if (!mutation.append().has_record()) { + creation_status = absl::InvalidArgumentError("No record specified for append mutation."); + return; + } + if (!mutation.append().record().value().has_string_value()) { + creation_status = + absl::InvalidArgumentError("Only string value is allowed for record value."); + return; + } + + auto value_or_error = + Formatter::FormatterImpl::create(mutation.append().record().value().string_value(), true); + SET_AND_RETURN_IF_NOT_OK(value_or_error.status(), creation_status); + query_query_parameter_mutations_.emplace_back(std::make_unique( + mutation.append().record().key(), std::move(value_or_error.value()), + mutation.append().action())); + + } else if (!mutation.remove().empty()) { + query_query_parameter_mutations_.emplace_back( + std::make_unique(mutation.remove())); + } else { + creation_status = absl::InvalidArgumentError("One of 'append'/'remove' must be specified."); + return; + } + } +} + +void Mutations::mutateRequestHeaders(Http::RequestHeaderMap& headers, + const Formatter::HttpFormatterContext& context, const StreamInfo::StreamInfo& stream_info) const { - request_mutations_->evaluateHeaders(headers, ctx, stream_info); + request_mutations_->evaluateHeaders(headers, context, stream_info); + + if (query_query_parameter_mutations_.empty() || headers.Path() == nullptr) { + return; + } + + // Mutate query parameters. + Http::Utility::QueryParamsMulti params = + Http::Utility::QueryParamsMulti::parseAndDecodeQueryString(headers.getPathValue()); + for (const auto& mutation : query_query_parameter_mutations_) { + mutation->mutateQueryParameter(params, context, stream_info); + } + headers.setPath(params.replaceQueryString(headers.Path()->value())); } -void Mutations::mutateResponseHeaders(Http::HeaderMap& headers, - const Formatter::HttpFormatterContext& ctx, +void Mutations::mutateResponseHeaders(Http::ResponseHeaderMap& headers, + const Formatter::HttpFormatterContext& context, const StreamInfo::StreamInfo& stream_info) const { - response_mutations_->evaluateHeaders(headers, ctx, stream_info); + response_mutations_->evaluateHeaders(headers, context, stream_info); } -PerRouteHeaderMutation::PerRouteHeaderMutation(const PerRouteProtoConfig& config) - : mutations_(config.mutations()) {} +PerRouteHeaderMutation::PerRouteHeaderMutation(const PerRouteProtoConfig& config, + absl::Status& creation_status) + : mutations_(config.mutations(), creation_status) {} -HeaderMutationConfig::HeaderMutationConfig(const ProtoConfig& config) - : mutations_(config.mutations()), +HeaderMutationConfig::HeaderMutationConfig(const ProtoConfig& config, absl::Status& creation_status) + : mutations_(config.mutations(), creation_status), most_specific_header_mutations_wins_(config.most_specific_header_mutations_wins()) {} -Http::FilterHeadersStatus HeaderMutation::decodeHeaders(Http::RequestHeaderMap& headers, bool) { - Formatter::HttpFormatterContext ctx{&headers}; - config_->mutations().mutateRequestHeaders(headers, ctx, decoder_callbacks_->streamInfo()); +void HeaderMutation::maybeInitializeRouteConfigs(Http::StreamFilterCallbacks* callbacks) { + // Ensure that route configs are initialized only once and the same route configs are used + // for both decoding and encoding paths. + // An independent flag is used to ensure even at the case where the route configs is empty, + // we still won't try to initialize it again. + if (route_configs_initialized_) { + return; + } + route_configs_initialized_ = true; // Traverse through all route configs to retrieve all available header mutations. // `getAllPerFilterConfig` returns in ascending order of specificity (i.e., route table // first, then virtual host, then per route). - route_configs_ = Http::Utility::getAllPerFilterConfig(decoder_callbacks_); - + route_configs_ = Http::Utility::getAllPerFilterConfig(callbacks); + + // The order of returned route configs is route table first, then virtual host, then + // per route. That means the per route configs will be evaluated at the end and will + // override the more general virtual host and route table configs. + // + // So, if most_specific_header_mutations_wins is false, we need to reverse the order + // of route configs. if (!config_->mostSpecificHeaderMutationsWins()) { - // most_specific_wins means that most specific level per filter config is evaluated last. In - // other words, header mutations are evaluated in ascending order of specificity (same order as - // `getAllPerFilterConfig` above returns). - // Thus, here we reverse iterate the vector when `most_specific_wins` is false. - for (auto it = route_configs_.rbegin(); it != route_configs_.rend(); ++it) { - (*it).get().mutations().mutateRequestHeaders(headers, ctx, decoder_callbacks_->streamInfo()); - } - } else { - for (const PerRouteHeaderMutation& route_config : route_configs_) { - route_config.mutations().mutateRequestHeaders(headers, ctx, decoder_callbacks_->streamInfo()); - } + std::reverse(route_configs_.begin(), route_configs_.end()); + } +} + +Http::FilterHeadersStatus HeaderMutation::decodeHeaders(Http::RequestHeaderMap& headers, bool) { + Formatter::HttpFormatterContext context{&headers}; + config_->mutations().mutateRequestHeaders(headers, context, decoder_callbacks_->streamInfo()); + + maybeInitializeRouteConfigs(decoder_callbacks_); + + for (const PerRouteHeaderMutation& route_config : route_configs_) { + route_config.mutations().mutateRequestHeaders(headers, context, + decoder_callbacks_->streamInfo()); } return Http::FilterHeadersStatus::Continue; } Http::FilterHeadersStatus HeaderMutation::encodeHeaders(Http::ResponseHeaderMap& headers, bool) { - Formatter::HttpFormatterContext ctx{encoder_callbacks_->requestHeaders().ptr(), &headers}; - config_->mutations().mutateResponseHeaders(headers, ctx, encoder_callbacks_->streamInfo()); + Formatter::HttpFormatterContext context{encoder_callbacks_->requestHeaders().ptr(), &headers}; + config_->mutations().mutateResponseHeaders(headers, context, encoder_callbacks_->streamInfo()); - // If we haven't already traversed the route configs, do so now. - if (route_configs_.empty()) { - route_configs_ = - Http::Utility::getAllPerFilterConfig(encoder_callbacks_); - } + // Note if the filter before this one has send local reply then the decodeHeaders() will not be + // be called and the route config will not be initialized. Try it again here and it will be + // no-op if already initialized. + maybeInitializeRouteConfigs(encoder_callbacks_); - if (!config_->mostSpecificHeaderMutationsWins()) { - for (auto it = route_configs_.rbegin(); it != route_configs_.rend(); ++it) { - (*it).get().mutations().mutateResponseHeaders(headers, ctx, encoder_callbacks_->streamInfo()); - } - } else { - for (const PerRouteHeaderMutation& route_config : route_configs_) { - route_config.mutations().mutateResponseHeaders(headers, ctx, - encoder_callbacks_->streamInfo()); - } + for (const PerRouteHeaderMutation& route_config : route_configs_) { + route_config.mutations().mutateResponseHeaders(headers, context, + encoder_callbacks_->streamInfo()); } return Http::FilterHeadersStatus::Continue; diff --git a/source/extensions/filters/http/header_mutation/header_mutation.h b/source/extensions/filters/http/header_mutation/header_mutation.h index 254c9ca07639..71c11164ed08 100644 --- a/source/extensions/filters/http/header_mutation/header_mutation.h +++ b/source/extensions/filters/http/header_mutation/header_mutation.h @@ -6,9 +6,12 @@ #include #include "envoy/extensions/filters/http/header_mutation/v3/header_mutation.pb.h" +#include "envoy/http/query_params.h" #include "source/common/common/logger.h" +#include "source/common/formatter/substitution_formatter.h" #include "source/common/http/header_mutation.h" +#include "source/common/http/utility.h" #include "source/extensions/filters/http/common/pass_through_filter.h" #include "absl/strings/string_view.h" @@ -22,31 +25,82 @@ using ProtoConfig = envoy::extensions::filters::http::header_mutation::v3::Heade using PerRouteProtoConfig = envoy::extensions::filters::http::header_mutation::v3::HeaderMutationPerRoute; using MutationsProto = envoy::extensions::filters::http::header_mutation::v3::Mutations; +using ParameterMutationProto = envoy::config::core::v3::KeyValueMutation; +using ParameterAppendProto = envoy::config::core::v3::KeyValueAppend; + +/** + * Interface for mutating query parameters. + */ +class QueryParameterMutation { +public: + virtual ~QueryParameterMutation() = default; + + /** + * Mutate the query parameters. + * @param params the query parameters to mutate. + * @param context the formatter context. + * @param stream_info the stream info. + */ + virtual void mutateQueryParameter(Http::Utility::QueryParamsMulti& params, + const Formatter::HttpFormatterContext& context, + const StreamInfo::StreamInfo& stream_info) const PURE; +}; +using QueryParameterMutationPtr = std::unique_ptr; + +class QueryParameterMutationRemove : public QueryParameterMutation { +public: + QueryParameterMutationRemove(absl::string_view key) : key_(key) {} + + // QueryParameterMutation + void mutateQueryParameter(Http::Utility::QueryParamsMulti& params, + const Formatter::HttpFormatterContext&, + const StreamInfo::StreamInfo&) const override { + params.remove(key_); + } + +private: + const std::string key_; +}; + +class QueryParameterMutationAppend : public QueryParameterMutation { +public: + QueryParameterMutationAppend(absl::string_view key, Formatter::FormatterPtr formatter, + ParameterAppendProto::KeyValueAppendAction action) + : key_(key), formatter_(std::move(formatter)), action_(action) {} + + // QueryParameterMutation + void mutateQueryParameter(Http::Utility::QueryParamsMulti& params, + const Formatter::HttpFormatterContext& context, + const StreamInfo::StreamInfo& stream_info) const override; + +private: + const std::string key_; + const Formatter::FormatterPtr formatter_; + const ParameterAppendProto::KeyValueAppendAction action_{}; +}; class Mutations { public: using HeaderMutations = Http::HeaderMutations; - Mutations(const MutationsProto& config) - : request_mutations_(THROW_OR_RETURN_VALUE( - HeaderMutations::create(config.request_mutations()), std::unique_ptr)), - response_mutations_( - THROW_OR_RETURN_VALUE(HeaderMutations::create(config.response_mutations()), - std::unique_ptr)) {} + Mutations(const MutationsProto& config, absl::Status& creation_status); - void mutateRequestHeaders(Http::HeaderMap& headers, const Formatter::HttpFormatterContext& ctx, + void mutateRequestHeaders(Http::RequestHeaderMap& headers, + const Formatter::HttpFormatterContext& context, const StreamInfo::StreamInfo& stream_info) const; - void mutateResponseHeaders(Http::HeaderMap& headers, const Formatter::HttpFormatterContext& ctx, + void mutateResponseHeaders(Http::ResponseHeaderMap& headers, + const Formatter::HttpFormatterContext& context, const StreamInfo::StreamInfo& stream_info) const; private: - const std::unique_ptr request_mutations_; - const std::unique_ptr response_mutations_; + std::unique_ptr request_mutations_; + std::vector query_query_parameter_mutations_; + std::unique_ptr response_mutations_; }; class PerRouteHeaderMutation : public Router::RouteSpecificFilterConfig { public: - PerRouteHeaderMutation(const PerRouteProtoConfig& config); + PerRouteHeaderMutation(const PerRouteProtoConfig& config, absl::Status& creation_status); const Mutations& mutations() const { return mutations_; } @@ -57,7 +111,7 @@ using PerRouteHeaderMutationSharedPtr = std::shared_ptr; class HeaderMutationConfig { public: - HeaderMutationConfig(const ProtoConfig& config); + HeaderMutationConfig(const ProtoConfig& config, absl::Status& creation_status); const Mutations& mutations() const { return mutations_; } @@ -80,8 +134,11 @@ class HeaderMutation : public Http::PassThroughFilter, public Logger::Loggable, 4> route_configs_{}; }; diff --git a/test/extensions/filters/http/header_mutation/config_test.cc b/test/extensions/filters/http/header_mutation/config_test.cc index 077a931ea9ca..e9fb792135f1 100644 --- a/test/extensions/filters/http/header_mutation/config_test.cc +++ b/test/extensions/filters/http/header_mutation/config_test.cc @@ -16,15 +16,14 @@ namespace HeaderMutation { namespace { TEST(FactoryTest, FactoryTest) { - - testing::NiceMock context; - + testing::NiceMock mock_factory_context; auto* factory = Registry::FactoryRegistry::getFactory( "envoy.filters.http.header_mutation"); ASSERT_NE(factory, nullptr); - const std::string config = R"EOF( + { + const std::string config = R"EOF( mutations: request_mutations: - remove: "flag-header" @@ -33,6 +32,13 @@ TEST(FactoryTest, FactoryTest) { key: "flag-header" value: "%REQ(ANOTHER-FLAG-HEADER)%" append_action: APPEND_IF_EXISTS_OR_ADD + query_parameter_mutations: + - remove: "flag-query" + - append: + record: + key: "flag-query" + value: "%REQ(ANOTHER-FLAG-QUERY)%" + action: APPEND_IF_EXISTS_OR_ADD response_mutations: - remove: "flag-header" - append: @@ -42,32 +48,125 @@ TEST(FactoryTest, FactoryTest) { append_action: APPEND_IF_EXISTS_OR_ADD )EOF"; - PerRouteProtoConfig per_route_proto_config; - TestUtility::loadFromYaml(config, per_route_proto_config); - ProtoConfig proto_config; - TestUtility::loadFromYaml(config, proto_config); + PerRouteProtoConfig per_route_proto_config; + TestUtility::loadFromYaml(config, per_route_proto_config); + ProtoConfig proto_config; + TestUtility::loadFromYaml(config, proto_config); + + auto cb = + factory->createFilterFactoryFromProto(proto_config, "test", mock_factory_context).value(); + Http::MockFilterChainFactoryCallbacks filter_callbacks; + EXPECT_CALL(filter_callbacks, addStreamFilter(_)); + cb(filter_callbacks); + + EXPECT_NE(nullptr, factory + ->createRouteSpecificFilterConfig( + per_route_proto_config, mock_factory_context.server_factory_context_, + mock_factory_context.messageValidationVisitor()) + .value()); + } + + { + const std::string config = R"EOF( + mutations: + query_parameter_mutations: + - remove: "" + )EOF"; - testing::NiceMock mock_factory_context; + ProtoConfig proto_config; + TestUtility::loadFromYaml(config, proto_config); + + auto cb_or_error = + factory->createFilterFactoryFromProto(proto_config, "test", mock_factory_context); + EXPECT_FALSE(cb_or_error.status().ok()); + EXPECT_EQ("One of 'append'/'remove' must be specified.", cb_or_error.status().message()); + } + + { + const std::string config = R"EOF( + mutations: + query_parameter_mutations: + - remove: "another-key" + append: + record: + key: "key" + value: "value" + )EOF"; + + ProtoConfig proto_config; + TestUtility::loadFromYaml(config, proto_config); + + auto cb_or_error = + factory->createFilterFactoryFromProto(proto_config, "test", mock_factory_context); + EXPECT_FALSE(cb_or_error.status().ok()); + EXPECT_EQ("Only one of 'append'/'remove can be specified.", cb_or_error.status().message()); + } + + { + const std::string config = R"EOF( + mutations: + query_parameter_mutations: + - append: {} + )EOF"; + + ProtoConfig proto_config; + TestUtility::loadFromYaml(config, proto_config); - auto cb = - factory->createFilterFactoryFromProto(proto_config, "test", mock_factory_context).value(); - Http::MockFilterChainFactoryCallbacks filter_callbacks; - EXPECT_CALL(filter_callbacks, addStreamFilter(_)); - cb(filter_callbacks); - - EXPECT_NE(nullptr, factory - ->createRouteSpecificFilterConfig( - per_route_proto_config, mock_factory_context.server_factory_context_, - mock_factory_context.messageValidationVisitor()) - .value()); + auto cb_or_error = + factory->createFilterFactoryFromProto(proto_config, "test", mock_factory_context); + EXPECT_FALSE(cb_or_error.status().ok()); + EXPECT_EQ("No record specified for append mutation.", cb_or_error.status().message()); + } + { + const std::string config = R"EOF( + mutations: + query_parameter_mutations: + - append: + record: + key: "key" + value: 123 + )EOF"; + + ProtoConfig proto_config; + TestUtility::loadFromYaml(config, proto_config); + + auto cb_or_error = + factory->createFilterFactoryFromProto(proto_config, "test", mock_factory_context); + EXPECT_FALSE(cb_or_error.status().ok()); + EXPECT_EQ("Only string value is allowed for record value.", cb_or_error.status().message()); + } + { + const std::string config = R"EOF( + mutations: + query_parameter_mutations: + - append: + record: + key: "key" + )EOF"; + + ProtoConfig proto_config; + TestUtility::loadFromYaml(config, proto_config); + + auto cb_or_error = + factory->createFilterFactoryFromProto(proto_config, "test", mock_factory_context); + EXPECT_FALSE(cb_or_error.status().ok()); + EXPECT_EQ("Only string value is allowed for record value.", cb_or_error.status().message()); + } } TEST(FactoryTest, UpstreamFactoryTest) { + auto* factory = + Registry::FactoryRegistry::getFactory( + "envoy.filters.http.header_mutation"); + ASSERT_NE(factory, nullptr); +} + +TEST(FactoryTest, QueryParameterMutationsTest) { testing::NiceMock context; auto* factory = - Registry::FactoryRegistry::getFactory( + Registry::FactoryRegistry::getFactory( "envoy.filters.http.header_mutation"); ASSERT_NE(factory, nullptr); } diff --git a/test/extensions/filters/http/header_mutation/header_mutation_test.cc b/test/extensions/filters/http/header_mutation/header_mutation_test.cc index 318f05d6c204..fcc6efd217c4 100644 --- a/test/extensions/filters/http/header_mutation/header_mutation_test.cc +++ b/test/extensions/filters/http/header_mutation/header_mutation_test.cc @@ -13,7 +13,7 @@ namespace { using testing::NiceMock; -TEST(HeaderMutationFilterTest, HeaderMutationFilterTest) { +TEST(HeaderMutationFilterTest, RequestMutationTest) { const std::string route_config_yaml = R"EOF( mutations: request_mutations: @@ -48,6 +48,79 @@ TEST(HeaderMutationFilterTest, HeaderMutationFilterTest) { key: "flag-header-6" value: "flag-header-6-value" append_action: "OVERWRITE_IF_EXISTS" + )EOF"; + + const std::string config_yaml = R"EOF( + mutations: + request_mutations: + - append: + header: + key: "global-flag-header" + value: "global-flag-header-value" + append_action: "ADD_IF_ABSENT" + )EOF"; + + PerRouteProtoConfig per_route_proto_config; + TestUtility::loadFromYaml(route_config_yaml, per_route_proto_config); + + absl::Status creation_status = absl::OkStatus(); + PerRouteHeaderMutationSharedPtr config = + std::make_shared(per_route_proto_config, creation_status); + + ProtoConfig proto_config; + TestUtility::loadFromYaml(config_yaml, proto_config); + HeaderMutationConfigSharedPtr global_config = + std::make_shared(proto_config, creation_status); + + { + NiceMock decoder_callbacks; + NiceMock encoder_callbacks; + + HeaderMutation filter{global_config}; + filter.setDecoderFilterCallbacks(decoder_callbacks); + filter.setEncoderFilterCallbacks(encoder_callbacks); + + EXPECT_CALL(*decoder_callbacks.route_, perFilterConfigs(_)) + .WillOnce(Invoke([&](absl::string_view) -> Router::RouteSpecificFilterConfigs { + return {config.get()}; + })); + + Envoy::Http::TestRequestHeaderMapImpl headers = { + {"flag-header", "flag-header-value"}, + {"another-flag-header", "another-flag-header-value"}, + {"flag-header-2", "flag-header-2-value-old"}, + {"flag-header-3", "flag-header-3-value-old"}, + {"flag-header-4", "flag-header-4-value-old"}, + {"flag-header-6", "flag-header-6-value-old"}, + {":method", "GET"}, + {":path", "/path"}, + {":scheme", "http"}, + {":authority", "host"}}; + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter.decodeHeaders(headers, true)); + + // 'flag-header' is removed and new 'flag-header' is added. + EXPECT_EQ("another-flag-header-value", headers.get_("flag-header")); + // 'flag-header-2' is appended. + EXPECT_EQ(2, headers.get(Envoy::Http::LowerCaseString("flag-header-2")).size()); + // 'flag-header-3' is not appended and keep the old value. + EXPECT_EQ(1, headers.get(Envoy::Http::LowerCaseString("flag-header-3")).size()); + EXPECT_EQ("flag-header-3-value-old", headers.get_("flag-header-3")); + // 'flag-header-4' is overwritten. + EXPECT_EQ(1, headers.get(Envoy::Http::LowerCaseString("flag-header-4")).size()); + EXPECT_EQ("flag-header-4-value", headers.get_("flag-header-4")); + // 'flag-header-5' was not present, so will not be present after mutation. + EXPECT_FALSE(headers.has("flag-header-5")); + // 'flag-header-6' was present and should be overwritten. + EXPECT_EQ("flag-header-6-value", headers.get_("flag-header-6")); + // global header is added. + EXPECT_EQ("global-flag-header-value", headers.get_("global-flag-header")); + } +} + +TEST(HeaderMutationFilterTest, ResponseMutationTest) { + const std::string route_config_yaml = R"EOF( + mutations: response_mutations: - remove: "flag-header" - append: @@ -84,12 +157,6 @@ TEST(HeaderMutationFilterTest, HeaderMutationFilterTest) { const std::string config_yaml = R"EOF( mutations: - request_mutations: - - append: - header: - key: "global-flag-header" - value: "global-flag-header-value" - append_action: "ADD_IF_ABSENT" response_mutations: - remove: "global-flag-header" )EOF"; @@ -97,14 +164,16 @@ TEST(HeaderMutationFilterTest, HeaderMutationFilterTest) { PerRouteProtoConfig per_route_proto_config; TestUtility::loadFromYaml(route_config_yaml, per_route_proto_config); + absl::Status creation_status = absl::OkStatus(); PerRouteHeaderMutationSharedPtr config = - std::make_shared(per_route_proto_config); + std::make_shared(per_route_proto_config, creation_status); ProtoConfig proto_config; TestUtility::loadFromYaml(config_yaml, proto_config); HeaderMutationConfigSharedPtr global_config = - std::make_shared(proto_config); + std::make_shared(proto_config, creation_status); + // Case where the decodeHeaders() is not called and the encodeHeaders() is called. { NiceMock decoder_callbacks; NiceMock encoder_callbacks; @@ -113,111 +182,49 @@ TEST(HeaderMutationFilterTest, HeaderMutationFilterTest) { filter.setDecoderFilterCallbacks(decoder_callbacks); filter.setEncoderFilterCallbacks(encoder_callbacks); - EXPECT_CALL(*decoder_callbacks.route_, perFilterConfigs(_)) + EXPECT_CALL(*encoder_callbacks.route_, perFilterConfigs(_)) .WillOnce(Invoke([&](absl::string_view) -> Router::RouteSpecificFilterConfigs { return {config.get()}; })); - { - Envoy::Http::TestRequestHeaderMapImpl headers = { - {"flag-header", "flag-header-value"}, - {"another-flag-header", "another-flag-header-value"}, - {"flag-header-2", "flag-header-2-value-old"}, - {"flag-header-3", "flag-header-3-value-old"}, - {"flag-header-4", "flag-header-4-value-old"}, - {"flag-header-6", "flag-header-6-value-old"}, - {":method", "GET"}, - {":path", "/"}, - {":scheme", "http"}, - {":authority", "host"}}; - - EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter.decodeHeaders(headers, true)); - - // 'flag-header' is removed and new 'flag-header' is added. - EXPECT_EQ("another-flag-header-value", headers.get_("flag-header")); - // 'flag-header-2' is appended. - EXPECT_EQ(2, headers.get(Envoy::Http::LowerCaseString("flag-header-2")).size()); - // 'flag-header-3' is not appended and keep the old value. - EXPECT_EQ(1, headers.get(Envoy::Http::LowerCaseString("flag-header-3")).size()); - EXPECT_EQ("flag-header-3-value-old", headers.get_("flag-header-3")); - // 'flag-header-4' is overwritten. - EXPECT_EQ(1, headers.get(Envoy::Http::LowerCaseString("flag-header-4")).size()); - EXPECT_EQ("flag-header-4-value", headers.get_("flag-header-4")); - // 'flag-header-5' was not present, so will not be present after mutation. - EXPECT_FALSE(headers.has("flag-header-5")); - // 'flag-header-6' was present and should be overwritten. - EXPECT_EQ("flag-header-6-value", headers.get_("flag-header-6")); - } - - // Case where the decodeHeaders() is not called and the encodeHeaders() is called. - { - Envoy::Http::TestResponseHeaderMapImpl headers = { - {"flag-header", "flag-header-value"}, - {"another-flag-header", "another-flag-header-value"}, - {"flag-header-2", "flag-header-2-value-old"}, - {"flag-header-3", "flag-header-3-value-old"}, - {"flag-header-4", "flag-header-4-value-old"}, - {"flag-header-6", "flag-header-6-value-old"}, - {":status", "200"}, - }; - - Http::RequestHeaderMap* request_headers_pointer = - Http::StaticEmptyHeaders::get().request_headers.get(); - EXPECT_CALL(encoder_callbacks, requestHeaders()) - .WillOnce(testing::Return(makeOptRefFromPtr(request_headers_pointer))); - - EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter.encodeHeaders(headers, true)); - - // 'flag-header' is removed and new 'flag-header' is added. - EXPECT_EQ("another-flag-header-value", headers.get_("flag-header")); - // 'flag-header-2' is appended. - EXPECT_EQ(2, headers.get(Envoy::Http::LowerCaseString("flag-header-2")).size()); - // 'flag-header-3' is not appended and keep the old value. - EXPECT_EQ(1, headers.get(Envoy::Http::LowerCaseString("flag-header-3")).size()); - EXPECT_EQ("flag-header-3-value-old", headers.get_("flag-header-3")); - // 'flag-header-4' is overwritten. - EXPECT_EQ(1, headers.get(Envoy::Http::LowerCaseString("flag-header-4")).size()); - EXPECT_EQ("flag-header-4-value", headers.get_("flag-header-4")); - // 'flag-header-5' was not present, so will not be present after mutation. - EXPECT_FALSE(headers.has("flag-header-5")); - // 'flag-header-6' was present and should be overwritten. - EXPECT_EQ("flag-header-6-value", headers.get_("flag-header-6")); - } - - // Case where the request headers map is nullptr. - { - Envoy::Http::TestResponseHeaderMapImpl headers = { - {"flag-header", "flag-header-value"}, - {"another-flag-header", "another-flag-header-value"}, - {"flag-header-2", "flag-header-2-value-old"}, - {"flag-header-3", "flag-header-3-value-old"}, - {"flag-header-4", "flag-header-4-value-old"}, - {"flag-header-6", "flag-header-6-value-old"}, - {":status", "200"}, - }; - - EXPECT_CALL(encoder_callbacks, requestHeaders()) - .WillOnce(testing::Return(Http::RequestHeaderMapOptRef{})); - - EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter.encodeHeaders(headers, true)); - - // 'flag-header' is removed and new 'flag-header' is added. - EXPECT_EQ("another-flag-header-value", headers.get_("flag-header")); - // 'flag-header-2' is appended. - EXPECT_EQ(2, headers.get(Envoy::Http::LowerCaseString("flag-header-2")).size()); - // 'flag-header-3' is not appended and keep the old value. - EXPECT_EQ(1, headers.get(Envoy::Http::LowerCaseString("flag-header-3")).size()); - EXPECT_EQ("flag-header-3-value-old", headers.get_("flag-header-3")); - // 'flag-header-4' is overwritten. - EXPECT_EQ(1, headers.get(Envoy::Http::LowerCaseString("flag-header-4")).size()); - EXPECT_EQ("flag-header-4-value", headers.get_("flag-header-4")); - // 'flag-header-5' was not present, so will not be present after mutation. - EXPECT_FALSE(headers.has("flag-header-5")); - // 'flag-header-6' was present and should be overwritten. - EXPECT_EQ("flag-header-6-value", headers.get_("flag-header-6")); - } + Envoy::Http::TestResponseHeaderMapImpl headers = { + {"flag-header", "flag-header-value"}, + {"another-flag-header", "another-flag-header-value"}, + {"global-flag-header", "global-flag-header-value"}, + {"flag-header-2", "flag-header-2-value-old"}, + {"flag-header-3", "flag-header-3-value-old"}, + {"flag-header-4", "flag-header-4-value-old"}, + {"flag-header-6", "flag-header-6-value-old"}, + {":status", "200"}, + }; + + Http::RequestHeaderMap* request_headers_pointer = + Http::StaticEmptyHeaders::get().request_headers.get(); + EXPECT_CALL(encoder_callbacks, requestHeaders()) + .WillOnce(testing::Return(makeOptRefFromPtr(request_headers_pointer))); + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter.encodeHeaders(headers, true)); + + // 'flag-header' is removed and new 'flag-header' is added. + EXPECT_EQ("another-flag-header-value", headers.get_("flag-header")); + // 'flag-header-2' is appended. + EXPECT_EQ(2, headers.get(Envoy::Http::LowerCaseString("flag-header-2")).size()); + // 'flag-header-3' is not appended and keep the old value. + EXPECT_EQ(1, headers.get(Envoy::Http::LowerCaseString("flag-header-3")).size()); + EXPECT_EQ("flag-header-3-value-old", headers.get_("flag-header-3")); + // 'flag-header-4' is overwritten. + EXPECT_EQ(1, headers.get(Envoy::Http::LowerCaseString("flag-header-4")).size()); + EXPECT_EQ("flag-header-4-value", headers.get_("flag-header-4")); + // 'flag-header-5' was not present, so will not be present after mutation. + EXPECT_FALSE(headers.has("flag-header-5")); + // 'flag-header-6' was present and should be overwritten. + EXPECT_EQ("flag-header-6-value", headers.get_("flag-header-6")); + // global header is removed. + EXPECT_FALSE(headers.has("global-flag-header")); } + // Case where the decodeHeaders() is not called and the encodeHeaders() is called and + // the request header map is nullptr. { NiceMock decoder_callbacks; NiceMock encoder_callbacks; @@ -226,9 +233,15 @@ TEST(HeaderMutationFilterTest, HeaderMutationFilterTest) { filter.setDecoderFilterCallbacks(decoder_callbacks); filter.setEncoderFilterCallbacks(encoder_callbacks); + EXPECT_CALL(*encoder_callbacks.route_, perFilterConfigs(_)) + .WillOnce(Invoke([&](absl::string_view) -> Router::RouteSpecificFilterConfigs { + return {config.get()}; + })); + Envoy::Http::TestResponseHeaderMapImpl headers = { {"flag-header", "flag-header-value"}, {"another-flag-header", "another-flag-header-value"}, + {"global-flag-header", "global-flag-header-value"}, {"flag-header-2", "flag-header-2-value-old"}, {"flag-header-3", "flag-header-3-value-old"}, {"flag-header-4", "flag-header-4-value-old"}, @@ -236,11 +249,8 @@ TEST(HeaderMutationFilterTest, HeaderMutationFilterTest) { {":status", "200"}, }; - // If the decoding phase is not performed then try to get the config from the encoding phase. - EXPECT_CALL(*encoder_callbacks.route_, perFilterConfigs(_)) - .WillOnce(Invoke([&](absl::string_view) -> Router::RouteSpecificFilterConfigs { - return {config.get()}; - })); + EXPECT_CALL(encoder_callbacks, requestHeaders()) + .WillOnce(testing::Return(Http::RequestHeaderMapOptRef{})); EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter.encodeHeaders(headers, true)); @@ -258,7 +268,59 @@ TEST(HeaderMutationFilterTest, HeaderMutationFilterTest) { EXPECT_FALSE(headers.has("flag-header-5")); // 'flag-header-6' was present and should be overwritten. EXPECT_EQ("flag-header-6-value", headers.get_("flag-header-6")); + // global header is removed. + EXPECT_FALSE(headers.has("global-flag-header")); } +} + +TEST(HeaderMutationFilterTest, HybridMutationTest) { + const std::string route_config_yaml = R"EOF( + mutations: + request_mutations: + - remove: "flag-header" + - append: + header: + key: "flag-header" + value: "%REQ(ANOTHER-FLAG-HEADER)%" + append_action: "APPEND_IF_EXISTS_OR_ADD" + response_mutations: + - remove: "flag-header" + - append: + header: + key: "flag-header" + value: "%RESP(ANOTHER-FLAG-HEADER)%" + append_action: "APPEND_IF_EXISTS_OR_ADD" + query_parameter_mutations: + - remove: "flag-query" + - append: + record: + key: "flag-query" + value: "%REQ(ANOTHER-FLAG-QUERY)%" + action: "APPEND_IF_EXISTS_OR_ADD" + )EOF"; + + const std::string config_yaml = R"EOF( + mutations: + request_mutations: + - append: + header: + key: "global-flag-header" + value: "global-flag-header-value" + response_mutations: + - remove: "global-flag-header" + )EOF"; + + PerRouteProtoConfig per_route_proto_config; + TestUtility::loadFromYaml(route_config_yaml, per_route_proto_config); + + absl::Status creation_status = absl::OkStatus(); + PerRouteHeaderMutationSharedPtr config = + std::make_shared(per_route_proto_config, creation_status); + + ProtoConfig proto_config; + TestUtility::loadFromYaml(config_yaml, proto_config); + HeaderMutationConfigSharedPtr global_config = + std::make_shared(proto_config, creation_status); { NiceMock decoder_callbacks; @@ -268,19 +330,159 @@ TEST(HeaderMutationFilterTest, HeaderMutationFilterTest) { filter.setDecoderFilterCallbacks(decoder_callbacks); filter.setEncoderFilterCallbacks(encoder_callbacks); + EXPECT_CALL(*decoder_callbacks.route_, perFilterConfigs(_)) + .WillOnce(Invoke([&](absl::string_view) -> Router::RouteSpecificFilterConfigs { + return {config.get()}; + })); + Envoy::Http::TestRequestHeaderMapImpl request_headers = { - {":method", "GET"}, {":path", "/"}, {":scheme", "http"}, {":authority", "host"}}; + {"flag-header", "flag-header-value"}, + {"another-flag-header", "another-flag-header-value"}, + {"another-flag-query", "another-flag-query-value"}, + {":method", "GET"}, + {":path", "/path?flag-query=flag-query-value"}, + {":scheme", "http"}, + {":authority", "host"}}; Envoy::Http::TestResponseHeaderMapImpl response_headers = { {"global-flag-header", "global-flag-header-value"}, + {"flag-header", "flag-header-value"}, + {"another-flag-header", "another-flag-header-value"}, {":status", "200"}, }; EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter.decodeHeaders(request_headers, true)); EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter.encodeHeaders(response_headers, true)); + // Request header mutation. + // 'flag-header' is removed and new 'flag-header' is added. + EXPECT_EQ("another-flag-header-value", request_headers.get_("flag-header")); + // global header is added. EXPECT_EQ("global-flag-header-value", request_headers.get_("global-flag-header")); - EXPECT_EQ("", response_headers.get_("global-flag-header")); + // 'flag-query' is removed and new 'flag-query' is added. + auto params = + Http::Utility::QueryParamsMulti::parseAndDecodeQueryString(request_headers.getPathValue()); + EXPECT_EQ("another-flag-query-value", params.data().at("flag-query").front()); + + // Response header mutation. + // 'flag-header' is removed and new 'flag-header' is added. + EXPECT_EQ("another-flag-header-value", response_headers.get_("flag-header")); + // global header is removed. + EXPECT_FALSE(response_headers.has("global-flag-header")); + } +} + +TEST(HeaderMutationFilterTest, QueryParameterMutationTest) { + const std::string route_config_yaml = R"EOF( + mutations: + query_parameter_mutations: + - remove: "flag-query" + - append: + record: + key: "flag-query" + value: "%REQ(ANOTHER-FLAG-QUERY)%" + action: "APPEND_IF_EXISTS_OR_ADD" + - append: + record: + key: "flag-query-2" + value: "flag-query-2-value" + action: "APPEND_IF_EXISTS_OR_ADD" + - append: + record: + key: "flag-query-3" + value: "flag-query-3-value" + action: "ADD_IF_ABSENT" + - append: + record: + key: "flag-query-4" + value: "flag-query-4-value" + action: "OVERWRITE_IF_EXISTS_OR_ADD" + - append: + record: + key: "flag-query-5" + value: "flag-query-5-value" + action: "OVERWRITE_IF_EXISTS" + - append: + record: + key: "flag-query-6" + value: "flag-query-6-value" + action: "OVERWRITE_IF_EXISTS" + )EOF"; + + const std::string config_yaml = R"EOF( + mutations: + request_mutations: + - append: + header: + key: "global-flag-header" + value: "global-flag-header-value" + append_action: "ADD_IF_ABSENT" + query_parameter_mutations: + - append: + record: + key: "global-param-key" + value: "global-param-value" + action: "ADD_IF_ABSENT" + response_mutations: + - remove: "global-flag-header" + )EOF"; + + PerRouteProtoConfig per_route_proto_config; + TestUtility::loadFromYaml(route_config_yaml, per_route_proto_config); + + absl::Status creation_status = absl::OkStatus(); + PerRouteHeaderMutationSharedPtr config = + std::make_shared(per_route_proto_config, creation_status); + + ProtoConfig proto_config; + TestUtility::loadFromYaml(config_yaml, proto_config); + HeaderMutationConfigSharedPtr global_config = + std::make_shared(proto_config, creation_status); + + { + NiceMock decoder_callbacks; + NiceMock encoder_callbacks; + + HeaderMutation filter{global_config}; + filter.setDecoderFilterCallbacks(decoder_callbacks); + filter.setEncoderFilterCallbacks(encoder_callbacks); + + EXPECT_CALL(*decoder_callbacks.route_, perFilterConfigs(_)) + .WillOnce(Invoke([&](absl::string_view) -> Router::RouteSpecificFilterConfigs { + return {config.get()}; + })); + + Envoy::Http::TestRequestHeaderMapImpl headers = { + {"another-flag-query", "another-flag-query-value"}, + {":method", "GET"}, + {":path", "/path?" + "flag-query=flag-query-value&" + "flag-query-2=flag-query-2-value-old&" + "flag-query-3=flag-query-3-value-old&" + "flag-query-4=flag-query-4-value-old&" + "flag-query-6=flag-query-6-value-old"}, + {":scheme", "http"}, + {":authority", "host"}}; + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter.decodeHeaders(headers, true)); + + auto params = + Http::Utility::QueryParamsMulti::parseAndDecodeQueryString(headers.getPathValue()); + // 'flag-query' is removed and new 'flag-query' is added. + EXPECT_EQ("another-flag-query-value", params.data().at("flag-query").front()); + // 'flag-query-2' is appended. + EXPECT_EQ(2, params.data().at("flag-query-2").size()); + // 'flag-query-3' is not appended and keep the old value. + EXPECT_EQ("flag-query-3-value-old", params.data().at("flag-query-3").front()); + // 'flag-query-4' is overwritten. + EXPECT_EQ(1, params.data().at("flag-query-4").size()); + EXPECT_EQ("flag-query-4-value", params.data().at("flag-query-4").front()); + // 'flag-query-5' was not present, so will not be present after mutation. + EXPECT_FALSE(params.data().contains("flag-query-5")); + // 'flag-query-6' was present and should be overwritten. + EXPECT_EQ("flag-query-6-value", params.data().at("flag-query-6").front()); + // global query parameter is added. + EXPECT_EQ("global-param-value", params.data().at("global-param-key").front()); } } diff --git a/tools/code_format/config.yaml b/tools/code_format/config.yaml index aa7fcd5c09ba..22fb42d8e0b7 100644 --- a/tools/code_format/config.yaml +++ b/tools/code_format/config.yaml @@ -182,7 +182,6 @@ paths: - source/extensions/filters/http/grpc_json_reverse_transcoder - source/extensions/filters/http/grpc_json_transcoder - source/extensions/filters/http/grpc_stats - - source/extensions/filters/http/header_mutation - source/extensions/filters/http/header_to_metadata - source/extensions/filters/http/health_check - source/extensions/filters/http/ip_tagging