diff --git a/api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto b/api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto index 4788afef2434a..87e629f4f441f 100644 --- a/api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto +++ b/api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto @@ -605,6 +605,7 @@ message LocalReplyConfig { } // The configuration to filter and change local response. +// [#next-free-field: 6] message ResponseMapper { // Filter to determine if this mapper should apply. config.accesslog.v3.AccessLogFilter filter = 1 [(validate.rules).message = {required: true}]; @@ -619,6 +620,11 @@ message ResponseMapper { // A per mapper `body_format` to override the :ref:`body_format `. // It will be used when this mapper is matched. config.core.v3.SubstitutionFormatString body_format_override = 4; + + // HTTP headers to add to a local reply. This allows the response mapper to append, to add + // or to override headers of any local reply before it is sent to a downstream client. + repeated config.core.v3.HeaderValueOption headers_to_add = 5 + [(validate.rules).repeated = {max_items: 1000}]; } message Rds { diff --git a/api/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto b/api/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto index 705f5e5fdcc6e..ac31bf1ecd62f 100644 --- a/api/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto +++ b/api/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto @@ -607,6 +607,7 @@ message LocalReplyConfig { } // The configuration to filter and change local response. +// [#next-free-field: 6] message ResponseMapper { option (udpa.annotations.versioning).previous_message_type = "envoy.extensions.filters.network.http_connection_manager.v3.ResponseMapper"; @@ -624,6 +625,11 @@ message ResponseMapper { // A per mapper `body_format` to override the :ref:`body_format `. // It will be used when this mapper is matched. config.core.v4alpha.SubstitutionFormatString body_format_override = 4; + + // HTTP headers to add to a local reply. This allows the response mapper to append, to add + // or to override headers of any local reply before it is sent to a downstream client. + repeated config.core.v4alpha.HeaderValueOption headers_to_add = 5 + [(validate.rules).repeated = {max_items: 1000}]; } message Rds { diff --git a/docs/root/configuration/http/http_conn_man/local_reply.rst b/docs/root/configuration/http/http_conn_man/local_reply.rst index c2f9d59e18d6a..5b87d9e3ef5ca 100644 --- a/docs/root/configuration/http/http_conn_man/local_reply.rst +++ b/docs/root/configuration/http/http_conn_man/local_reply.rst @@ -15,7 +15,7 @@ Features: Local reply content modification -------------------------------- -The local response content returned by Envoy can be customized. A list of :ref:`mappers ` can be specified. Each mapper must have a :ref:`filter `. It may have following rewrite rules; a :ref:`status_code ` rule to rewrite response code, a :ref:`body ` rule to rewrite the local reply body and a :ref:`body_format_override ` to specify the response body format. Envoy checks each `mapper` according to the specified order until the first one is matched. If a `mapper` is matched, all its rewrite rules will apply. +The local response content returned by Envoy can be customized. A list of :ref:`mappers ` can be specified. Each mapper must have a :ref:`filter `. It may have following rewrite rules; a :ref:`status_code ` rule to rewrite response code, a :ref:`headers_to_add ` rule to add/override/append response HTTP headers, a :ref:`body ` rule to rewrite the local reply body and a :ref:`body_format_override ` to specify the response body format. Envoy checks each `mapper` according to the specified order until the first one is matched. If a `mapper` is matched, all its rewrite rules will apply. Example of a LocalReplyConfig @@ -29,6 +29,11 @@ Example of a LocalReplyConfig value: default_value: 400 runtime_key: key_b + headers_to_add: + - header: + key: "foo" + value: "bar" + append: false status_code: 401 body: inline_string: "not allowed" diff --git a/docs/root/version_history/current.rst b/docs/root/version_history/current.rst index eb3465f99eebe..13b638ba9ad16 100644 --- a/docs/root/version_history/current.rst +++ b/docs/root/version_history/current.rst @@ -10,6 +10,7 @@ Minor Behavior Changes *Changes that may cause incompatibilities for some users, but should not for most* * compressor: always insert `Vary` headers for compressible resources even if it's decided not to compress a response due to incompatible `Accept-Encoding` value. The `Vary` header needs to be inserted to let a caching proxy in front of Envoy know that the requested resource still can be served with compression applied. +* http: added :ref:`headers_to_add ` to :ref:`local reply mapper ` to allow its users to add/append/override response HTTP headers to local replies. * http: added HCM level configuration of :ref:`error handling on invalid messaging ` which substantially changes Envoy's behavior when encountering invalid HTTP/1.1 defaulting to closing the connection instead of allowing reuse. This can temporarily be reverted by setting `envoy.reloadable_features.hcm_stream_error_on_invalid_message` to false, or permanently reverted by setting the :ref:`HCM option ` to true to restore prior HTTP/1.1 beavior and setting the *new* HTTP/2 configuration :ref:`override_stream_error_on_invalid_http_message ` to false to retain prior HTTP/2 behavior. * http: the per-stream FilterState maintained by the HTTP connection manager will now provide read/write access to the downstream connection FilterState. As such, code that relies on interacting with this might see a change in behavior. diff --git a/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto b/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto index 6d505f748222c..a25759c85fc7e 100644 --- a/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto +++ b/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto @@ -610,6 +610,7 @@ message LocalReplyConfig { } // The configuration to filter and change local response. +// [#next-free-field: 6] message ResponseMapper { // Filter to determine if this mapper should apply. config.accesslog.v3.AccessLogFilter filter = 1 [(validate.rules).message = {required: true}]; @@ -624,6 +625,11 @@ message ResponseMapper { // A per mapper `body_format` to override the :ref:`body_format `. // It will be used when this mapper is matched. config.core.v3.SubstitutionFormatString body_format_override = 4; + + // HTTP headers to add to a local reply. This allows the response mapper to append, to add + // or to override headers of any local reply before it is sent to a downstream client. + repeated config.core.v3.HeaderValueOption headers_to_add = 5 + [(validate.rules).repeated = {max_items: 1000}]; } message Rds { diff --git a/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto b/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto index 705f5e5fdcc6e..ac31bf1ecd62f 100644 --- a/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto +++ b/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto @@ -607,6 +607,7 @@ message LocalReplyConfig { } // The configuration to filter and change local response. +// [#next-free-field: 6] message ResponseMapper { option (udpa.annotations.versioning).previous_message_type = "envoy.extensions.filters.network.http_connection_manager.v3.ResponseMapper"; @@ -624,6 +625,11 @@ message ResponseMapper { // A per mapper `body_format` to override the :ref:`body_format `. // It will be used when this mapper is matched. config.core.v4alpha.SubstitutionFormatString body_format_override = 4; + + // HTTP headers to add to a local reply. This allows the response mapper to append, to add + // or to override headers of any local reply before it is sent to a downstream client. + repeated config.core.v4alpha.HeaderValueOption headers_to_add = 5 + [(validate.rules).repeated = {max_items: 1000}]; } message Rds { diff --git a/source/common/local_reply/BUILD b/source/common/local_reply/BUILD index 6de00f364e0c0..16995a49ea862 100644 --- a/source/common/local_reply/BUILD +++ b/source/common/local_reply/BUILD @@ -23,6 +23,7 @@ envoy_cc_library( "//source/common/formatter:substitution_format_string_lib", "//source/common/formatter:substitution_formatter_lib", "//source/common/http:header_map_lib", + "//source/common/router:header_parser_lib", "//source/common/stream_info:stream_info_lib", "@envoy_api//envoy/extensions/filters/network/http_connection_manager/v3:pkg_cc_proto", ], diff --git a/source/common/local_reply/local_reply.cc b/source/common/local_reply/local_reply.cc index 9574de79c6fde..d4549dc1a1356 100644 --- a/source/common/local_reply/local_reply.cc +++ b/source/common/local_reply/local_reply.cc @@ -9,6 +9,7 @@ #include "common/formatter/substitution_format_string.h" #include "common/formatter/substitution_formatter.h" #include "common/http/header_map_impl.h" +#include "common/router/header_parser.h" namespace Envoy { namespace LocalReply { @@ -43,6 +44,7 @@ class BodyFormatter { }; using BodyFormatterPtr = std::unique_ptr; +using HeaderParserPtr = std::unique_ptr; class ResponseMapper { public: @@ -63,6 +65,8 @@ class ResponseMapper { if (config.has_body_format_override()) { body_formatter_ = std::make_unique(config.body_format_override()); } + + header_parser_ = Envoy::Router::HeaderParser::configure(config.headers_to_add()); } bool matchAndRewrite(const Http::RequestHeaderMap& request_headers, @@ -79,6 +83,8 @@ class ResponseMapper { body = body_.value(); } + header_parser_->evaluateHeaders(response_headers, stream_info); + if (status_code_.has_value() && code != status_code_.value()) { code = status_code_.value(); response_headers.setStatus(std::to_string(enumToInt(code))); @@ -95,6 +101,7 @@ class ResponseMapper { const AccessLog::FilterPtr filter_; absl::optional status_code_; absl::optional body_; + HeaderParserPtr header_parser_; BodyFormatterPtr body_formatter_; }; diff --git a/test/common/local_reply/local_reply_test.cc b/test/common/local_reply/local_reply_test.cc index 2bf9149d0a943..0807a12982cdb 100644 --- a/test/common/local_reply/local_reply_test.cc +++ b/test/common/local_reply/local_reply_test.cc @@ -291,5 +291,49 @@ TEST_F(LocalReplyTest, TestMapperFormat) { EXPECT_EQ(content_type_, "text/plain"); } +TEST_F(LocalReplyTest, TestHeaderAddition) { + // Default text formatter without any mappers + const std::string yaml = R"( + mappers: + - filter: + status_code_filter: + comparison: + op: GE + value: + default_value: 0 + runtime_key: key_b + headers_to_add: + - header: + key: foo-1 + value: bar1 + append: true + - header: + key: foo-2 + value: override-bar2 + append: false + - header: + key: foo-3 + value: append-bar3 + append: true +)"; + TestUtility::loadFromYaml(yaml, config_); + auto local = Factory::create(config_, context_); + + response_headers_.addCopy("foo-2", "bar2"); + response_headers_.addCopy("foo-3", "bar3"); + local->rewrite(nullptr, response_headers_, stream_info_, code_, body_, content_type_); + EXPECT_EQ(code_, TestInitCode); + EXPECT_EQ(stream_info_.response_code_, static_cast(TestInitCode)); + EXPECT_EQ(content_type_, "text/plain"); + + EXPECT_EQ(response_headers_.get_("foo-1"), "bar1"); + EXPECT_EQ(response_headers_.get_("foo-2"), "override-bar2"); + std::vector out; + Http::HeaderUtility::getAllOfHeader(response_headers_, "foo-3", out); + ASSERT_EQ(out.size(), 2); + ASSERT_EQ(out[0], "bar3"); + ASSERT_EQ(out[1], "append-bar3"); +} + } // namespace LocalReply } // namespace Envoy diff --git a/test/integration/local_reply_integration_test.cc b/test/integration/local_reply_integration_test.cc index 472aaf8220be2..dacd7fcad033b 100644 --- a/test/integration/local_reply_integration_test.cc +++ b/test/integration/local_reply_integration_test.cc @@ -28,6 +28,11 @@ TEST_P(LocalReplyIntegrationTest, MapStatusCodeAndFormatToJson) { name: test-header exact_match: exact-match-value status_code: 550 + headers_to_add: + - header: + key: foo + value: bar + append: false body_format: json_format: level: TRACE @@ -74,6 +79,7 @@ TEST_P(LocalReplyIntegrationTest, MapStatusCodeAndFormatToJson) { EXPECT_EQ("application/json", response->headers().ContentType()->value().getStringView()); EXPECT_EQ("150", response->headers().ContentLength()->value().getStringView()); EXPECT_EQ("550", response->headers().Status()->value().getStringView()); + EXPECT_EQ("bar", response->headers().get(Http::LowerCaseString("foo"))->value().getStringView()); // Check if returned json is same as expected EXPECT_TRUE(TestUtility::jsonStringEqual(response->body(), expected_body)); } @@ -131,7 +137,7 @@ TEST_P(LocalReplyIntegrationTest, MapStatusCodeAndFormatToJson4Grpc) { expected_grpc_message)); } -// Matched second filter has code and body rewrite and its format +// Matched second filter has code, headers and body rewrite and its format TEST_P(LocalReplyIntegrationTest, MapStatusCodeAndFormatToJsonForFirstMatchingFilter) { const std::string yaml = R"EOF( mappers: @@ -147,6 +153,11 @@ TEST_P(LocalReplyIntegrationTest, MapStatusCodeAndFormatToJsonForFirstMatchingFi name: test-header exact_match: exact-match-value status_code: 551 + headers_to_add: + - header: + key: foo + value: bar + append: false body: inline_string: "customized body text" body_format_override: @@ -199,6 +210,7 @@ TEST_P(LocalReplyIntegrationTest, MapStatusCodeAndFormatToJsonForFirstMatchingFi EXPECT_EQ("text/plain", response->headers().ContentType()->value().getStringView()); EXPECT_EQ("24", response->headers().ContentLength()->value().getStringView()); EXPECT_EQ("551", response->headers().Status()->value().getStringView()); + EXPECT_EQ("bar", response->headers().get(Http::LowerCaseString("foo"))->value().getStringView()); // Check if returned json is same as expected EXPECT_EQ(response->body(), expected_body); }