diff --git a/source/common/http/async_client_impl.h b/source/common/http/async_client_impl.h index 9e248c5d57182..e0a1e3fb3c23d 100644 --- a/source/common/http/async_client_impl.h +++ b/source/common/http/async_client_impl.h @@ -384,7 +384,7 @@ class AsyncStreamImpl : public AsyncClient::Stream, } Utility::sendLocalReply( remote_closed_, - Utility::EncodeFunctions{nullptr, nullptr, + Utility::EncodeFunctions{nullptr, nullptr, nullptr, [this, modify_headers, &details](ResponseHeaderMapPtr&& headers, bool end_stream) -> void { if (modify_headers != nullptr) { diff --git a/source/common/http/filter_manager.cc b/source/common/http/filter_manager.cc index 6caf33f7536fb..965e4e20bba56 100644 --- a/source/common/http/filter_manager.cc +++ b/source/common/http/filter_manager.cc @@ -862,6 +862,12 @@ void FilterManager::sendLocalReplyViaFilterChain( // a no-op. createFilterChain(); + // A bit awkward, but save the local reply data for later. This will come in handy. + // When this pointer is non-null, it means a local reply has been initiated, which + // is important later on. + local_reply_data_ = std::make_unique( + Utility::LocalReplyData{is_grpc_request, code, body, grpc_status, is_head_request}); + Utility::sendLocalReply( state_.destroyed_, Utility::EncodeFunctions{ @@ -875,13 +881,14 @@ void FilterManager::sendLocalReplyViaFilterChain( modify_headers(headers); } }, - [this](ResponseHeaderMap& response_headers, Code& code, std::string& body, - absl::string_view& content_type) -> void { - // TODO(snowp): This &get() business isn't nice, rework LocalReply and others to accept - // opt refs. - local_reply_.rewrite(filter_manager_callbacks_.requestHeaders().ptr(), response_headers, - stream_info_, code, body, content_type); - }, + // Local reply rewrites (and upstream rewrites, for that matter) happen on the filter + // chain now, so we do not need to do any additional rewriting here. + /*rewrite_=*/nullptr, + // Local gRPC replies are encoded as trailers-only responses on the filter chain, + // so we don't need to any additional encoding of the gRPC response here. This + // is different from the direct local reply case, where we do encode the gRPC + // reply as a headers-only response using an encoder callback. + /*encode_grpc_=*/nullptr, [this, modify_headers](ResponseHeaderMapPtr&& headers, bool end_stream) -> void { filter_manager_callbacks_.setResponseHeaders(std::move(headers)); // TODO: Start encoding from the last decoder filter that saw the @@ -918,9 +925,18 @@ void FilterManager::sendDirectLocalReply( }, [&](ResponseHeaderMap& response_headers, Code& code, std::string& body, absl::string_view& content_type) -> void { + /* Direct local reply rewrites happen here since we are not going through the filter + * chain. */ local_reply_.rewrite(filter_manager_callbacks_.requestHeaders().ptr(), response_headers, stream_info_, code, body, content_type); }, + [&](ResponseHeaderMap& headers, Code& code, std::string& body, + const absl::optional grpc_status, + bool is_head_request) -> void { + /* Direct local reply gRPC encoding happens here since we are not going through the + * filter chain. */ + Utility::toGrpcTrailersOnlyResponse(headers, code, body, grpc_status, is_head_request); + }, [&](ResponseHeaderMapPtr&& response_headers, bool end_stream) -> void { // Move the response headers into the FilterManager to make sure they're visible to // access logs. @@ -1044,9 +1060,55 @@ void FilterManager::encodeHeaders(ActiveStreamEncoderFilter* filter, ResponseHea } } - const bool modified_end_stream = (end_stream && continue_data_entry == encoder_filters_.end()); + bool modified_end_stream = (end_stream && continue_data_entry == encoder_filters_.end()); + + // Capture the original modified_end_stream value. We may modify it again below if we add + // a response body. + const bool original_modified_end_stream = modified_end_stream; + + // See if we should do an upstream rewrite. For local replies, always try to do a rewrite. + // For upstream replies, only try to do a rewrite if some filter rule actually applies. + // The rewrite config supports unconditional rewrites with no filter rules. This makes sense + // for local reply rewrites (e.g. one consistent format for all responses generated at Envoy) + // but makes a lot less sense for all responses upstream. + state_.do_response_rewrite_ = + local_reply_data_ != nullptr || + local_reply_.matchesAnyMapper(filter_manager_callbacks_.requestHeaders().ptr(), + *filter_manager_callbacks_.responseHeaders(), + filter_manager_callbacks_.responseTrailers().ptr(), + stream_info_); + ENVOY_STREAM_LOG(debug, + "FilterManager::encodeHeaders: end_stream={}, modified_end_stream={}, " + "state_.do_response_rewrite_={}", + *this, end_stream, modified_end_stream, state_.do_response_rewrite_); + + if (state_.do_response_rewrite_) { + // Try to rewrite the response. This may end up not doing any actual rewrite if this is + // a local reply and there is no matching rule. If it's not a local reply, we know there's + // a match at this point since that's a condition for state_.do_response_rewrite_ above. + rewriteResponse(); + + if (buffered_response_data_) { + // If we have a rewritten response body, then modified_end_stream can no longer be true + // because we have a body now. + modified_end_stream = false; + } + } + state_.non_100_response_headers_encoded_ = true; filter_manager_callbacks_.encodeHeaders(headers, modified_end_stream); + + // Encode the rewritten response right away if the original `modified_end_stream` was true. + // If it wasn't, then we know a body will be encoded later, and we'll let that function + // take care of encoding the rewritten response. + if (state_.do_response_rewrite_ && original_modified_end_stream && buffered_response_data_) { + ENVOY_STREAM_LOG(trace, + "FilterManager::encodeData calling filter_manager_callbacks_ with {} bytes " + "from buffered_response_data and modified_end_stream={}", + *this, buffered_response_data_->length(), modified_end_stream); + filter_manager_callbacks_.encodeData(*buffered_response_data_, modified_end_stream); + } + maybeEndEncode(modified_end_stream); if (!modified_end_stream) { @@ -1180,7 +1242,19 @@ void FilterManager::encodeData(ActiveStreamEncoderFilter* filter, Buffer::Instan } const bool modified_end_stream = end_stream && trailers_added_entry == encoder_filters_.end(); - filter_manager_callbacks_.encodeData(data, modified_end_stream); + if (state_.do_response_rewrite_ && buffered_response_data_) { + // If we're doing a rewrite and modified_end_stream=true, encode the buffered_response_data_ + // that was set by rewriteResponse() earlier in encodeHeaders(). + if (modified_end_stream) { + ENVOY_STREAM_LOG(trace, + "FilterManager::encodeData calling filter_manager_callbacks_ with {} bytes " + "from buffered_response_data and modified_end_stream={}", + *this, buffered_response_data_->length(), modified_end_stream); + filter_manager_callbacks_.encodeData(*buffered_response_data_, modified_end_stream); + } + } else { + filter_manager_callbacks_.encodeData(data, modified_end_stream); + } maybeEndEncode(modified_end_stream); // If trailers were adding during encodeData we need to trigger decodeTrailers in order @@ -1225,6 +1299,58 @@ void FilterManager::maybeEndEncode(bool end_stream) { } } +void FilterManager::rewriteResponse() { + auto response_headers = filter_manager_callbacks_.responseHeaders(); + ASSERT(response_headers.ptr() != nullptr); + + std::string rewritten_body{}; + absl::string_view rewritten_content_type{}; + Http::Code rewritten_code{static_cast(Utility::getResponseStatus(*response_headers))}; + + // Start with the local reply body and text/plain content type, if we have it. + if (local_reply_data_) { + rewritten_body = local_reply_data_->body_text_; + rewritten_content_type = Headers::get().ContentTypeValues.Text; + } + + // Get rid of any buffered response data we may have at this point. We're going to use this + // as the output parameter for a rewritten body, if any. + buffered_response_data_ = nullptr; + + ENVOY_STREAM_LOG(trace, "rewriteResponse: calling local_reply_.rewrite with body=\"{}\", code={}", + *this, rewritten_body, rewritten_code); + const bool did_body_rewrite = + local_reply_.rewrite(filter_manager_callbacks_.requestHeaders().ptr(), *response_headers, + stream_info_, rewritten_code, rewritten_body, rewritten_content_type); + ENVOY_STREAM_LOG( + trace, + "rewriteResponse: local_reply_.rewrite returned did_body_rewrite={}, body=\"{}\", " + "content_type={}, code={}", + *this, did_body_rewrite, rewritten_body, rewritten_content_type, rewritten_code); + + if (local_reply_data_ && local_reply_data_->is_grpc_) { + // Send a trailers-only grpc response + Utility::toGrpcTrailersOnlyResponse(*response_headers, rewritten_code, rewritten_body, + local_reply_data_->grpc_status_, + local_reply_data_->is_head_request_); + return; + } + + if (did_body_rewrite) { + buffered_response_data_ = std::make_unique(rewritten_body); + + // If we rewrote with a non-empty body, set the content length and type. + // Otherwise, remove them. + if (buffered_response_data_->length() > 0) { + response_headers->setContentLength(buffered_response_data_->length()); + response_headers->setContentType(rewritten_content_type); + } else { + response_headers->removeContentLength(); + response_headers->removeContentType(); + } + } +} + bool FilterManager::processNewlyAddedMetadata() { if (request_metadata_map_vector_ == nullptr) { return false; diff --git a/source/common/http/filter_manager.h b/source/common/http/filter_manager.h index 80845e0f9375a..56204da77f0d4 100644 --- a/source/common/http/filter_manager.h +++ b/source/common/http/filter_manager.h @@ -20,6 +20,7 @@ #include "common/grpc/common.h" #include "common/http/header_utility.h" #include "common/http/headers.h" +#include "common/http/utility.h" #include "common/local_reply/local_reply.h" #include "common/matcher/matcher.h" #include "common/protobuf/utility.h" @@ -909,6 +910,11 @@ class FilterManager : public ScopeTrackedObject, */ void maybeEndEncode(bool end_stream); + /** + * Rewrite the response headers and body using local_reply_. + */ + void rewriteResponse(); + void sendLocalReply(bool is_grpc_request, Code code, absl::string_view body, const std::function& modify_headers, const absl::optional grpc_status, @@ -1075,7 +1081,12 @@ class FilterManager : public ScopeTrackedObject, std::make_shared(); FilterChainFactory& filter_chain_factory_; + // Used to track local reply state + // TODO(esmet): We could combine the local reply reference and data into one pointer + // member that stays nullptr when no local reply config is present to save space in + // the 'off' case. But, for now, we spend two pointers of space in all cases. const LocalReply::LocalReply& local_reply_; + Utility::LocalReplyDataPtr local_reply_data_; OverridableRemoteSocketAddressSetterStreamInfo stream_info_; // TODO(snowp): Once FM has been moved to its own file we'll make these private classes of FM, // at which point they no longer need to be friends. @@ -1109,7 +1120,7 @@ class FilterManager : public ScopeTrackedObject, State() : remote_complete_(false), local_complete_(false), has_continue_headers_(false), created_filter_chain_(false), is_head_request_(false), is_grpc_request_(false), - non_100_response_headers_encoded_(false) {} + non_100_response_headers_encoded_(false), do_response_rewrite_(false) {} uint32_t filter_call_state_{0}; @@ -1127,6 +1138,8 @@ class FilterManager : public ScopeTrackedObject, bool is_grpc_request_ : 1; // Tracks if headers other than 100-Continue have been encoded to the codec. bool non_100_response_headers_encoded_ : 1; + // True if we should rewrite the upstream response using local_reply_ + bool do_response_rewrite_ : 1; // The following 3 members are booleans rather than part of the space-saving bitfield as they // are passed as arguments to functions expecting bools. Extend State using the bitfield diff --git a/source/common/http/utility.cc b/source/common/http/utility.cc index 86660479c683a..60c45136138c7 100644 --- a/source/common/http/utility.cc +++ b/source/common/http/utility.cc @@ -512,7 +512,7 @@ void Utility::sendLocalReply(const bool& is_reset, StreamDecoderFilterCallbacks& sendLocalReply( is_reset, - Utility::EncodeFunctions{nullptr, nullptr, + Utility::EncodeFunctions{nullptr, nullptr, nullptr, [&](ResponseHeaderMapPtr&& headers, bool end_stream) -> void { callbacks.encodeHeaders(std::move(headers), end_stream, details); }, @@ -522,6 +522,27 @@ void Utility::sendLocalReply(const bool& is_reset, StreamDecoderFilterCallbacks& local_reply_data); } +void Utility::toGrpcTrailersOnlyResponse(Http::ResponseHeaderMap& response_headers, + const Http::Code& code, std::string& response_body, + const absl::optional grpc_status, + bool is_head_request) { + response_headers.setStatus(std::to_string(enumToInt(Http::Code::OK))); + response_headers.setReferenceContentType(Headers::get().ContentTypeValues.Grpc); + response_headers.setGrpcStatus(std::to_string(enumToInt( + grpc_status ? grpc_status.value() : Grpc::Utility::httpToGrpcStatus(enumToInt(code))))); + if (!response_body.empty() && !is_head_request) { + // TODO(dio): Probably it is worth to consider caching the encoded message based on gRPC + // status. + // JsonFormatter adds a '\n' at the end. For header value, it should be removed. + // https://github.com/envoyproxy/envoy/blob/main/source/common/formatter/substitution_formatter.cc#L129 + if (response_body[response_body.length() - 1] == '\n') { + response_body = response_body.substr(0, response_body.length() - 1); + } + response_headers.setGrpcMessage(Http::Utility::PercentEncoding::encode(response_body)); + } + response_headers.removeContentLength(); +} + void Utility::sendLocalReply(const bool& is_reset, const EncodeFunctions& encode_functions, const LocalReplyData& local_reply_data) { // encode_headers() may reset the stream, so the stream must not be reset before calling it. @@ -542,26 +563,15 @@ void Utility::sendLocalReply(const bool& is_reset, const EncodeFunctions& encode encode_functions.rewrite_(*response_headers, response_code, body_text, content_type); } - // Respond with a gRPC trailers-only response if the request is gRPC + // Respond with a gRPC trailers-only response if the request is gRPC. The actual encoding of the + // trailers-only response happens in encode_grpc_, if set. Otherwise it is assumed that the + // response is already encoded and ready to be finalized by encodeHeaders with end_stream=true. if (local_reply_data.is_grpc_) { - response_headers->setStatus(std::to_string(enumToInt(Code::OK))); - response_headers->setReferenceContentType(Headers::get().ContentTypeValues.Grpc); - response_headers->setGrpcStatus( - std::to_string(enumToInt(local_reply_data.grpc_status_ - ? local_reply_data.grpc_status_.value() - : Grpc::Utility::httpToGrpcStatus(enumToInt(response_code))))); - if (!body_text.empty() && !local_reply_data.is_head_request_) { - // TODO(dio): Probably it is worth to consider caching the encoded message based on gRPC - // status. - // JsonFormatter adds a '\n' at the end. For header value, it should be removed. - // https://github.com/envoyproxy/envoy/blob/main/source/common/formatter/substitution_formatter.cc#L129 - if (body_text[body_text.length() - 1] == '\n') { - body_text = body_text.substr(0, body_text.length() - 1); - } - response_headers->setGrpcMessage(PercentEncoding::encode(body_text)); + if (encode_functions.encode_grpc_) { + encode_functions.encode_grpc_(*response_headers, response_code, body_text, + local_reply_data.grpc_status_, + local_reply_data.is_head_request_); } - // The `modify_headers` function may have added content-length, remove it. - response_headers->removeContentLength(); encode_functions.encode_headers_(std::move(response_headers), true); // Trailers only response return; } diff --git a/source/common/http/utility.h b/source/common/http/utility.h index d677b097d86cb..62001803b9be7 100644 --- a/source/common/http/utility.h +++ b/source/common/http/utility.h @@ -295,6 +295,11 @@ struct EncodeFunctions { std::function rewrite_; + // Function to encode a gRPC response. + std::function grpc_status, + bool is_head_request)> + encode_grpc_; // Function to encode response headers. std::function encode_headers_; // Function to encode the response body. @@ -313,6 +318,7 @@ struct LocalReplyData { // Tells if this is a response to a HEAD request. bool is_head_request_ = false; }; +using LocalReplyDataPtr = std::unique_ptr; /** * Create a locally generated response using filter callbacks. @@ -337,6 +343,18 @@ void sendLocalReply(const bool& is_reset, StreamDecoderFilterCallbacks& callback void sendLocalReply(const bool& is_reset, const EncodeFunctions& encode_functions, const LocalReplyData& local_reply_data); +/** + * Convert a response into a gRPC trailers-only response. + * @param response_headers the response headers. will be modified. + * @param code the upstream response code. will be converted to grpc-status + * @param grpc_status the original grpc status + * @param is_head_request whether this is a HEAD request + */ +void toGrpcTrailersOnlyResponse(Http::ResponseHeaderMap& response_headers, const Http::Code& code, + std::string& response_body, + const absl::optional grpc_status, + bool is_head_request); + struct GetLastAddressFromXffInfo { // Last valid address pulled from the XFF header. Network::Address::InstanceConstSharedPtr address_; diff --git a/source/common/local_reply/local_reply.cc b/source/common/local_reply/local_reply.cc index 42f8d32b0d383..e0d9b8806263d 100644 --- a/source/common/local_reply/local_reply.cc +++ b/source/common/local_reply/local_reply.cc @@ -11,6 +11,7 @@ #include "common/formatter/substitution_format_string.h" #include "common/formatter/substitution_formatter.h" #include "common/http/header_map_impl.h" +#include "common/http/utility.h" #include "common/router/header_parser.h" namespace Envoy { @@ -80,10 +81,48 @@ class ResponseMapper { StreamInfo::StreamInfo& stream_info, Http::Code& code, std::string& body, BodyFormatter*& final_formatter) const { // If not matched, just bail out. - if (!filter_->evaluate(stream_info, request_headers, response_headers, response_trailers)) { + if (!match(&request_headers, response_headers, &response_trailers, stream_info)) { return false; } + rewrite(request_headers, response_headers, stream_info, code, body, final_formatter); + return true; + } + + // Decide if a request/response pair matches this mapper. + bool match(const Http::RequestHeaderMap* request_headers, + const Http::ResponseHeaderMap& response_headers, + const Http::ResponseTrailerMap* response_trailers, + StreamInfo::StreamInfo& stream_info) const { + // Set response code on the stream_info because it's used by the StatusCode filter. + // Further, we know that the status header present on the upstream response headers + // is the status we want to match on. It may not be the status we send downstream + // to the client, though, because we may call `rewrite` later. + // + // Under normal circumstances we should have a response status by this point, because + // either the upstream set it or the router filter set it. If for whatever reason we + // don't, skip setting the stream info's response code and just let our evaluation + // logic do without it. We can't do much better, and we certainly don't want to throw + // an exception and crash here. + if (response_headers.Status() != nullptr) { + stream_info.setResponseCode( + static_cast(Http::Utility::getResponseStatus(response_headers))); + } + + if (request_headers == nullptr) { + request_headers = Http::StaticEmptyHeaders::get().request_headers.get(); + } + + if (response_trailers == nullptr) { + response_trailers = Http::StaticEmptyHeaders::get().response_trailers.get(); + } + + return filter_->evaluate(stream_info, *request_headers, response_headers, *response_trailers); + } + + void rewrite(const Http::RequestHeaderMap&, Http::ResponseHeaderMap& response_headers, + StreamInfo::StreamInfo& stream_info, Http::Code& code, std::string& body, + BodyFormatter*& final_formatter) const { if (body_.has_value()) { body = body_.value(); } @@ -99,7 +138,6 @@ class ResponseMapper { if (body_formatter_) { final_formatter = body_formatter_.get(); } - return true; } private: @@ -114,7 +152,8 @@ using ResponseMapperPtr = std::unique_ptr; class LocalReplyImpl : public LocalReply { public: - LocalReplyImpl() : body_formatter_(std::make_unique()) {} + LocalReplyImpl() + : body_formatter_(std::make_unique()), has_configured_body_formatter_(false) {} LocalReplyImpl( const envoy::extensions::filters::network::http_connection_manager::v3::LocalReplyConfig& @@ -122,13 +161,26 @@ class LocalReplyImpl : public LocalReply { Server::Configuration::FactoryContext& context) : body_formatter_(config.has_body_format() ? std::make_unique(config.body_format(), context.api()) - : std::make_unique()) { + : std::make_unique()), + has_configured_body_formatter_(config.has_body_format()) { for (const auto& mapper : config.mappers()) { mappers_.emplace_back(std::make_unique(mapper, context)); } } - void rewrite(const Http::RequestHeaderMap* request_headers, + bool matchesAnyMapper(const Http::RequestHeaderMap* request_headers, + const Http::ResponseHeaderMap& response_headers, + const Http::ResponseTrailerMap* response_trailers, + StreamInfo::StreamInfo& stream_info) const override { + for (const auto& mapper : mappers_) { + if (mapper->match(request_headers, response_headers, response_trailers, stream_info)) { + return true; + } + } + return false; + } + + bool rewrite(const Http::RequestHeaderMap* request_headers, Http::ResponseHeaderMap& response_headers, StreamInfo::StreamInfo& stream_info, Http::Code& code, std::string& body, absl::string_view& content_type) const override { @@ -154,14 +206,18 @@ class LocalReplyImpl : public LocalReply { if (!final_formatter) { final_formatter = body_formatter_.get(); } - return final_formatter->format(*request_headers, response_headers, - *Http::StaticEmptyHeaders::get().response_trailers, stream_info, - body, content_type); + final_formatter->format(*request_headers, response_headers, + *Http::StaticEmptyHeaders::get().response_trailers, stream_info, body, + content_type); + // If this local reply has a configured body formatter or the final formatter is not the + // default formatter, then we know `body` was modified by an explicitly configured formatter. + return has_configured_body_formatter_ || final_formatter != body_formatter_.get(); } private: std::list mappers_; const BodyFormatterPtr body_formatter_; + bool has_configured_body_formatter_; }; LocalReplyPtr Factory::createDefault() { return std::make_unique(); } diff --git a/source/common/local_reply/local_reply.h b/source/common/local_reply/local_reply.h index 5db93caa07fda..d0eb3edf4a53c 100644 --- a/source/common/local_reply/local_reply.h +++ b/source/common/local_reply/local_reply.h @@ -21,11 +21,25 @@ class LocalReply { * @param code status code. * @param body response body. * @param content_type response content_type. + * @return whether the local reply used a non-default body formatter to populate `body` */ - virtual void rewrite(const Http::RequestHeaderMap* request_headers, + virtual bool rewrite(const Http::RequestHeaderMap* request_headers, Http::ResponseHeaderMap& response_headers, StreamInfo::StreamInfo& stream_info, Http::Code& code, std::string& body, absl::string_view& content_type) const PURE; + + /** + * decide if any mapper matches the request/response. + * @param request_headers the request headers. + * @param response_headers the response headers. + * @param response_trailers the response trailers, if any. + * @param stream_info the stream info. + * @return whether any mapper is a match + */ + virtual bool matchesAnyMapper(const Http::RequestHeaderMap* request_headers, + const Http::ResponseHeaderMap& response_headers, + const Http::ResponseTrailerMap* response_trailers, + StreamInfo::StreamInfo& stream_info) const PURE; }; using LocalReplyPtr = std::unique_ptr; diff --git a/test/common/local_reply/local_reply_test.cc b/test/common/local_reply/local_reply_test.cc index 64afad622bd1a..73384b373af88 100644 --- a/test/common/local_reply/local_reply_test.cc +++ b/test/common/local_reply/local_reply_test.cc @@ -29,6 +29,9 @@ class LocalReplyTest : public testing::Test { code_ = code; body_ = TestInitBody; content_type_ = TestInitContentType; + // LocalReply::matchesAnyMapper() is used in a context where response_headers are assumed to + // exist // so we have to set the response code in the headers here. + response_headers_.setStatus(std::to_string(enumToInt(code))); } void resetData(uint32_t code) { resetData(static_cast(code)); } @@ -49,6 +52,8 @@ TEST_F(LocalReplyTest, TestEmptyConfig) { // Empty LocalReply config. auto local = Factory::create(config_, context_); + EXPECT_EQ(false, + local->matchesAnyMapper(&request_headers_, response_headers_, nullptr, stream_info_)); local->rewrite(nullptr, response_headers_, stream_info_, code_, body_, content_type_); EXPECT_EQ(code_, TestInitCode); EXPECT_EQ(stream_info_.response_code_, static_cast(TestInitCode)); @@ -62,6 +67,8 @@ TEST_F(LocalReplyTest, TestDefaultLocalReply) { // Default LocalReply should be the same as empty config. auto local = Factory::createDefault(); + EXPECT_EQ(false, + local->matchesAnyMapper(&request_headers_, response_headers_, nullptr, stream_info_)); local->rewrite(nullptr, response_headers_, stream_info_, code_, body_, content_type_); EXPECT_EQ(code_, TestInitCode); EXPECT_EQ(stream_info_.response_code_, static_cast(TestInitCode)); @@ -112,6 +119,8 @@ TEST_F(LocalReplyTest, TestDefaultTextFormatter) { TestUtility::loadFromYaml(yaml, config_); auto local = Factory::create(config_, context_); + EXPECT_EQ(false, + local->matchesAnyMapper(&request_headers_, response_headers_, nullptr, stream_info_)); local->rewrite(nullptr, response_headers_, stream_info_, code_, body_, content_type_); EXPECT_EQ(code_, TestInitCode); EXPECT_EQ(stream_info_.response_code_, static_cast(TestInitCode)); @@ -134,6 +143,8 @@ TEST_F(LocalReplyTest, TestDefaultJsonFormatter) { TestUtility::loadFromYaml(yaml, config_); auto local = Factory::create(config_, context_); + EXPECT_EQ(false, + local->matchesAnyMapper(&request_headers_, response_headers_, nullptr, stream_info_)); local->rewrite(&request_headers_, response_headers_, stream_info_, code_, body_, content_type_); EXPECT_EQ(code_, TestInitCode); EXPECT_EQ(stream_info_.response_code_, static_cast(TestInitCode)); @@ -203,6 +214,8 @@ TEST_F(LocalReplyTest, TestMapperRewrite) { // code=400 matches the first filter; rewrite code and body resetData(400); + EXPECT_EQ(true, + local->matchesAnyMapper(&request_headers_, response_headers_, nullptr, stream_info_)); local->rewrite(&request_headers_, response_headers_, stream_info_, code_, body_, content_type_); EXPECT_EQ(code_, static_cast(401)); EXPECT_EQ(stream_info_.response_code_, 401U); @@ -213,6 +226,8 @@ TEST_F(LocalReplyTest, TestMapperRewrite) { // code=403 matches the second filter; does not rewrite code, sets an empty body and content_type. resetData(403); body_ = "original body text"; + EXPECT_EQ(true, + local->matchesAnyMapper(&request_headers_, response_headers_, nullptr, stream_info_)); local->rewrite(&request_headers_, response_headers_, stream_info_, code_, body_, content_type_); EXPECT_EQ(code_, static_cast(403)); EXPECT_EQ(stream_info_.response_code_, 403U); @@ -222,6 +237,8 @@ TEST_F(LocalReplyTest, TestMapperRewrite) { // code=410 matches the third filter; rewrite body only resetData(410); + EXPECT_EQ(true, + local->matchesAnyMapper(&request_headers_, response_headers_, nullptr, stream_info_)); local->rewrite(&request_headers_, response_headers_, stream_info_, code_, body_, content_type_); EXPECT_EQ(code_, static_cast(410)); EXPECT_EQ(stream_info_.response_code_, 410U); @@ -231,6 +248,8 @@ TEST_F(LocalReplyTest, TestMapperRewrite) { // code=420 matches the fourth filter; rewrite code only resetData(420); + EXPECT_EQ(true, + local->matchesAnyMapper(&request_headers_, response_headers_, nullptr, stream_info_)); local->rewrite(&request_headers_, response_headers_, stream_info_, code_, body_, content_type_); EXPECT_EQ(code_, static_cast(421)); EXPECT_EQ(stream_info_.response_code_, 421U); @@ -240,6 +259,8 @@ TEST_F(LocalReplyTest, TestMapperRewrite) { // code=430 matches the fifth filter; rewrite nothing resetData(430); + EXPECT_EQ(true, + local->matchesAnyMapper(&request_headers_, response_headers_, nullptr, stream_info_)); local->rewrite(&request_headers_, response_headers_, stream_info_, code_, body_, content_type_); EXPECT_EQ(code_, static_cast(430)); EXPECT_EQ(stream_info_.response_code_, 430U); @@ -271,6 +292,8 @@ TEST_F(LocalReplyTest, DEPRECATED_FEATURE_TEST(TestMapperRewriteDeprecatedTextFo // code=404 matches the only filter; does not rewrite code, sets an empty body and content_type. resetData(404); body_ = "original body text"; + EXPECT_EQ(true, + local->matchesAnyMapper(&request_headers_, response_headers_, nullptr, stream_info_)); local->rewrite(&request_headers_, response_headers_, stream_info_, code_, body_, content_type_); EXPECT_EQ(code_, static_cast(404)); EXPECT_EQ(stream_info_.response_code_, 404U); @@ -319,6 +342,8 @@ TEST_F(LocalReplyTest, TestMapperFormat) { // code=400 matches the first filter; rewrite code and body // has its own formatter resetData(400); + EXPECT_EQ(true, + local->matchesAnyMapper(&request_headers_, response_headers_, nullptr, stream_info_)); local->rewrite(&request_headers_, response_headers_, stream_info_, code_, body_, content_type_); EXPECT_EQ(code_, static_cast(401)); EXPECT_EQ(stream_info_.response_code_, 401U); @@ -336,6 +361,8 @@ TEST_F(LocalReplyTest, TestMapperFormat) { // code=410 matches the second filter; rewrite code and body // but using default formatter resetData(410); + EXPECT_EQ(true, + local->matchesAnyMapper(&request_headers_, response_headers_, nullptr, stream_info_)); local->rewrite(&request_headers_, response_headers_, stream_info_, code_, body_, content_type_); EXPECT_EQ(code_, static_cast(411)); EXPECT_EQ(stream_info_.response_code_, 411U); @@ -374,6 +401,8 @@ TEST_F(LocalReplyTest, TestHeaderAddition) { response_headers_.addCopy("foo-2", "bar2"); response_headers_.addCopy("foo-3", "bar3"); + EXPECT_EQ(true, + local->matchesAnyMapper(&request_headers_, response_headers_, nullptr, stream_info_)); local->rewrite(nullptr, response_headers_, stream_info_, code_, body_, content_type_); EXPECT_EQ(code_, TestInitCode); EXPECT_EQ(stream_info_.response_code_, static_cast(TestInitCode)); @@ -440,6 +469,8 @@ TEST_F(LocalReplyTest, TestMapperWithContentType) { // has its own formatter. // content-type is explicitly set to text/html; charset=UTF-8. resetData(400); + EXPECT_EQ(true, + local->matchesAnyMapper(&request_headers_, response_headers_, nullptr, stream_info_)); local->rewrite(&request_headers_, response_headers_, stream_info_, code_, body_, content_type_); EXPECT_EQ(code_, static_cast(401)); EXPECT_EQ(stream_info_.response_code_, 401U); @@ -451,6 +482,8 @@ TEST_F(LocalReplyTest, TestMapperWithContentType) { // but using default formatter. // content-type is explicitly set to text/html; charset=UTF-8. resetData(410); + EXPECT_EQ(true, + local->matchesAnyMapper(&request_headers_, response_headers_, nullptr, stream_info_)); local->rewrite(&request_headers_, response_headers_, stream_info_, code_, body_, content_type_); EXPECT_EQ(code_, static_cast(411)); EXPECT_EQ(stream_info_.response_code_, 411U); @@ -462,7 +495,10 @@ TEST_F(LocalReplyTest, TestMapperWithContentType) { // has its own formatter. // default content-type is set based on reply format type. resetData(420); - local->rewrite(&request_headers_, response_headers_, stream_info_, code_, body_, content_type_); + EXPECT_EQ(true, + local->matchesAnyMapper(&request_headers_, response_headers_, nullptr, stream_info_)); + EXPECT_EQ(true, local->rewrite(&request_headers_, response_headers_, stream_info_, code_, body_, + content_type_)); EXPECT_EQ(code_, static_cast(421)); EXPECT_EQ(stream_info_.response_code_, 421U); EXPECT_EQ(response_headers_.Status()->value().getStringView(), "421"); diff --git a/test/integration/fake_upstream.h b/test/integration/fake_upstream.h index 1eb98c10c7f16..548d67163307d 100644 --- a/test/integration/fake_upstream.h +++ b/test/integration/fake_upstream.h @@ -105,7 +105,7 @@ class FakeStream : public Http::RequestDecoder, Http::Utility::sendLocalReply( false, Http::Utility::EncodeFunctions( - {nullptr, nullptr, + {nullptr, nullptr, nullptr, [&](Http::ResponseHeaderMapPtr&& headers, bool end_stream) -> void { encoder_.encodeHeaders(*headers, end_stream); }, diff --git a/test/integration/local_reply_integration_test.cc b/test/integration/local_reply_integration_test.cc index 8bf22764eb101..ce155f5f6d81d 100644 --- a/test/integration/local_reply_integration_test.cc +++ b/test/integration/local_reply_integration_test.cc @@ -13,6 +13,39 @@ class LocalReplyIntegrationTest : public HttpProtocolIntegrationTest { TestUtility::loadFromYaml(yaml, local_reply_config); config_helper_.setLocalReply(local_reply_config); } + + IntegrationStreamDecoderPtr getUpstreamResponse(const std::string& yaml, + const std::string& response_code, + uint64_t upstream_body_size, + const std::string& method = "GET") { + setLocalReplyConfig(yaml); + initialize(); + + codec_client_ = makeHttpConnection(lookupPort("http")); + + auto response = codec_client_->makeHeaderOnlyRequest( + Http::TestRequestHeaderMapImpl{{":method", method}, + {":path", "/"}, + {":scheme", "http"}, + {":authority", "host"}, + {"test-header", "exact-match-value"}}); + waitForNextUpstreamRequest(); + + if (upstream_body_size > 0) { + upstream_request_->encodeHeaders( + Http::TestResponseHeaderMapImpl{{":status", response_code}, + {"content-type", "application/original"}, + {"content-length", std::to_string(upstream_body_size)}}, + false); + upstream_request_->encodeData(upstream_body_size, true); + } else { + upstream_request_->encodeHeaders( + Http::TestResponseHeaderMapImpl{{":status", response_code}, {"content-length", "0"}}, + true); + } + response->waitForHeaders(); + return response; + } }; INSTANTIATE_TEST_SUITE_P(Protocols, LocalReplyIntegrationTest, @@ -466,4 +499,322 @@ TEST_P(LocalReplyIntegrationTest, ShouldFormatResponseToEmptyBody) { EXPECT_EQ(response->body(), ""); } +const std::string match_501_yaml = R"EOF( +mappers: +- filter: + status_code_filter: + comparison: + op: EQ + value: + default_value: 501 + runtime_key: key_b + headers_to_add: + - header: + key: x-upstream-5xx + value: "1" + append: true + status_code: 500 +body_format: + text_format_source: + inline_string: "%RESPONSE_CODE%: %RESPONSE_CODE_DETAILS%" +)EOF"; + +// Should not match the http response code and not add a header nor modify (add) the body. +TEST_P(LocalReplyIntegrationTest, LocalyReplyNoMatchNoUpstreamBody) { + auto response = getUpstreamResponse(match_501_yaml, "200", 0, "GET"); + + EXPECT_EQ("200", response->headers().Status()->value().getStringView()); + EXPECT_EQ(0, response->headers().get(Http::LowerCaseString("x-upstream-5xx")).size()); + EXPECT_EQ("", response->body()); + EXPECT_EQ("", response->headers().getContentTypeValue()); + + cleanupUpstreamAndDownstream(); +} + +// Should not match the http response code and not add a header nor modify (replace) the body. +TEST_P(LocalReplyIntegrationTest, LocalyReplyNoMatchWithUpstreamBody) { + auto response = getUpstreamResponse(match_501_yaml, "200", 8, "GET"); + + EXPECT_EQ("200", response->headers().Status()->value().getStringView()); + EXPECT_EQ(0, response->headers().get(Http::LowerCaseString("x-upstream-5xx")).size()); + EXPECT_EQ(std::string(8, 'a'), response->body()); + EXPECT_EQ("application/original", response->headers().getContentTypeValue()); + EXPECT_EQ("8", response->headers().getContentLengthValue()); + + cleanupUpstreamAndDownstream(); +} + +const std::string match_429_rewrite_450_yaml = R"EOF( +mappers: +- filter: + status_code_filter: + comparison: + op: EQ + value: + default_value: 429 + runtime_key: key_b + status_code: 450 +body_format: + text_format_source: + inline_string: "%RESPONSE_CODE%: %RESPONSE_CODE_DETAILS%" +)EOF"; + +// Should match an http response code and modify (add) the upstream response. +TEST_P(LocalReplyIntegrationTest, LocalyReplyMatchUpstreamStatusNoBody) { + auto response = getUpstreamResponse(match_429_rewrite_450_yaml, "429", 0); + + EXPECT_EQ("450", response->headers().Status()->value().getStringView()); + EXPECT_EQ(response->body(), "450: via_upstream"); + EXPECT_EQ("text/plain", response->headers().getContentTypeValue()); + EXPECT_EQ("17", response->headers().getContentLengthValue()); + + cleanupUpstreamAndDownstream(); +} + +// Should match an http response code and modify (replace) the upstream response. +TEST_P(LocalReplyIntegrationTest, LocalyReplyMatchUpstreamStatusWithBody) { + auto response = getUpstreamResponse(match_429_rewrite_450_yaml, "429", 512); + + EXPECT_EQ("450", response->headers().Status()->value().getStringView()); + EXPECT_EQ("450: via_upstream", response->body()); + EXPECT_EQ("text/plain", response->headers().getContentTypeValue()); + EXPECT_EQ("17", response->headers().getContentLengthValue()); + + cleanupUpstreamAndDownstream(); +} + +const std::string match_429_rewrite_451_remove_body_yaml = R"EOF( +mappers: +- filter: + status_code_filter: + comparison: + op: EQ + value: + default_value: 429 + runtime_key: key_b + status_code: 451 +body_format: + text_format_source: + inline_string: "" +)EOF"; + +// Should match an http response code and remove the response body (no body from upstream). +TEST_P(LocalReplyIntegrationTest, LocalyReplyMatchUpstreamStatusRemoveEmptyBody) { + auto response = getUpstreamResponse(match_429_rewrite_451_remove_body_yaml, "429", 0); + + EXPECT_EQ("451", response->headers().Status()->value().getStringView()); + EXPECT_EQ("", response->body()); + EXPECT_EQ("", response->headers().getContentTypeValue()); + + cleanupUpstreamAndDownstream(); +} + +// Should match an http response code and remove the response body (non-empty body from upstream). +TEST_P(LocalReplyIntegrationTest, LocalyReplyMatchUpstreamStatusRemoveNonEmptyBody) { + auto response = getUpstreamResponse(match_429_rewrite_451_remove_body_yaml, "429", 512); + + EXPECT_EQ("451", response->headers().Status()->value().getStringView()); + EXPECT_EQ("", response->body()); + EXPECT_EQ("", response->headers().getContentTypeValue()); + EXPECT_EQ("", response->headers().getContentLengthValue()); + + cleanupUpstreamAndDownstream(); +} + +const std::string match_429_rewrite_475_unmodified_body_yaml = R"EOF( +mappers: +- filter: + status_code_filter: + comparison: + op: EQ + value: + default_value: 429 + runtime_key: key_b + status_code: 475 +)EOF"; + +// Should match an http response code and rewrite status without modifying upstream body (no body +// from upstream). +TEST_P(LocalReplyIntegrationTest, LocalyReplyMatchUpstreamStatusWithUnmodifiedEmptyBody) { + auto response = getUpstreamResponse(match_429_rewrite_475_unmodified_body_yaml, "429", 0); + + EXPECT_EQ("475", response->headers().Status()->value().getStringView()); + // The unmodified upstream body is empty and has no content type. + EXPECT_EQ("", response->body()); + EXPECT_EQ("", response->headers().getContentTypeValue()); + + cleanupUpstreamAndDownstream(); +} + +// Should match an http response code and rewrite status without modifying upstream body (non-empty +// body from upstream). +TEST_P(LocalReplyIntegrationTest, LocalyReplyMatchUpstreamStatusWithUnmodifiedNonEmptyBody) { + auto response = getUpstreamResponse(match_429_rewrite_475_unmodified_body_yaml, "429", 8); + + EXPECT_EQ("475", response->headers().Status()->value().getStringView()); + EXPECT_EQ(std::string(8, 'a'), response->body()); + EXPECT_EQ("application/original", response->headers().getContentTypeValue()); + EXPECT_EQ("8", response->headers().getContentLengthValue()); + + cleanupUpstreamAndDownstream(); +} + +const std::string match_test_header_rewrite_435_yaml = R"EOF( +mappers: +- filter: + header_filter: + header: + name: test-header + exact_match: exact-match-value + status_code: 435 +body_format: + text_format_source: + inline_string: "%RESPONSE_CODE%: %RESPONSE_CODE_DETAILS%" +)EOF"; + +// Should match an http header, rewrite the response code, and modify the upstream response body (no +// upstream response body) +TEST_P(LocalReplyIntegrationTest, LocalyReplyMatchUpstreamHeaderNoBody) { + auto response = getUpstreamResponse(match_test_header_rewrite_435_yaml, "200", 0); + + EXPECT_EQ("435", response->headers().Status()->value().getStringView()); + EXPECT_EQ("435: via_upstream", response->body()); + EXPECT_EQ("text/plain", response->headers().getContentTypeValue()); + EXPECT_EQ("17", response->headers().getContentLengthValue()); + + cleanupUpstreamAndDownstream(); +} + +// Should match an http header, rewrite the response code, and modify the upstream response body +// (non-empty upstream response body) +TEST_P(LocalReplyIntegrationTest, LocalyReplyMatchUpstreamHeaderWithBody) { + auto response = getUpstreamResponse(match_test_header_rewrite_435_yaml, "200", 512); + + EXPECT_EQ("435", response->headers().Status()->value().getStringView()); + EXPECT_EQ("435: via_upstream", response->body()); + EXPECT_EQ("text/plain", response->headers().getContentTypeValue()); + EXPECT_EQ("17", response->headers().getContentLengthValue()); + + cleanupUpstreamAndDownstream(); +} + +const std::string match_500_rewrite_content_type_yaml = R"EOF( +mappers: +- filter: + status_code_filter: + comparison: + op: EQ + value: + default_value: 500 + runtime_key: key_b +body_format: + content_type: application/custom + text_format_source: + inline_string: "%RESPONSE_CODE%: %RESPONSE_CODE_DETAILS%" +)EOF"; + +// Should match an http header, rewrite the content type, and modify the upstream response body (no +// upstream response body) +TEST_P(LocalReplyIntegrationTest, LocalyReplyMatchUpstreamHeaderRewriteContentTypeNoBody) { + auto response = getUpstreamResponse(match_500_rewrite_content_type_yaml, "500", 0); + + EXPECT_EQ("500", response->headers().Status()->value().getStringView()); + EXPECT_EQ("500: via_upstream", response->body()); + EXPECT_EQ("application/custom", response->headers().getContentTypeValue()); + EXPECT_EQ("34", response->headers().getContentLengthValue()); + + cleanupUpstreamAndDownstream(); +} + +// Should match an http header, rewrite the content type, and modify the upstream response body +// (non-empty upstream response body) +TEST_P(LocalReplyIntegrationTest, LocalyReplyMatchUpstreamHeaderRewriteContentTypeWithBody) { + auto response = getUpstreamResponse(match_500_rewrite_content_type_yaml, "500", 512); + + EXPECT_EQ("500", response->headers().Status()->value().getStringView()); + EXPECT_EQ("500: via_upstream", response->body()); + EXPECT_EQ("application/custom", response->headers().getContentTypeValue()); + EXPECT_EQ("34", response->headers().getContentLengthValue()); + + cleanupUpstreamAndDownstream(); +} + +const std::string match_400_rewrite_json_yaml = R"EOF( +mappers: +- filter: + status_code_filter: + comparison: + op: EQ + value: + default_value: 400 + runtime_key: key_b +body_format: + json_format: + response_code: "%RESPONSE_CODE%" + details: "%RESPONSE_CODE_DETAILS%" +)EOF"; + +// Should match an http header, rewrite the content type, and modify the upstream response body +// (non-empty upstream response body) +TEST_P(LocalReplyIntegrationTest, LocalyReplyRewriteToJsonNoBody) { + auto response = getUpstreamResponse(match_400_rewrite_json_yaml, "400", 0); + + EXPECT_EQ("400", response->headers().Status()->value().getStringView()); + EXPECT_TRUE(TestUtility::jsonStringEqual(response->body(), + "{\"response_code\":400,\"details\":\"via_upstream\"}")); + EXPECT_EQ("application/json", response->headers().getContentTypeValue()); + EXPECT_EQ("47", response->headers().getContentLengthValue()); + + cleanupUpstreamAndDownstream(); +} + +// Should match an http header, rewrite the content type, and modify the upstream response body +// (non-empty upstream response body) +TEST_P(LocalReplyIntegrationTest, LocalyReplyRewriteToJsonWithBody) { + auto response = getUpstreamResponse(match_400_rewrite_json_yaml, "400", 512); + + EXPECT_EQ("400", response->headers().Status()->value().getStringView()); + EXPECT_EQ("application/json", response->headers().getContentTypeValue()); + EXPECT_TRUE(TestUtility::jsonStringEqual(response->body(), + "{\"response_code\":400,\"details\":\"via_upstream\"}")); + EXPECT_EQ("47", response->headers().getContentLengthValue()); + + cleanupUpstreamAndDownstream(); +} + +// Should match an http response code, add a response header, but not add any body to +// a HEAD response, even though a body format is configured. +TEST_P(LocalReplyIntegrationTest, LocalyReplyMatchUpstreamHeadResponseAddHeaderNoBody) { + const std::string yaml = R"EOF( +mappers: +- filter: + status_code_filter: + comparison: + op: EQ + value: + default_value: 500 + runtime_key: key_b + headers_to_add: + - header: + key: x-upstream-500 + value: "1" + append: true +body_format: + text_format_source: + inline_string: "%RESPONSE_CODE%: %RESPONSE_CODE_DETAILS%" +)EOF"; + auto response = getUpstreamResponse(yaml, "500", 0, "HEAD"); + + EXPECT_EQ("500", response->headers().Status()->value().getStringView()); + EXPECT_EQ(1, response->headers().get(Http::LowerCaseString("x-upstream-500")).size()); + EXPECT_EQ( + "1", + response->headers().get(Http::LowerCaseString("x-upstream-500"))[0]->value().getStringView()); + EXPECT_EQ("", response->body()); + EXPECT_EQ("text/plain", response->headers().getContentTypeValue()); + EXPECT_EQ("17", response->headers().getContentLengthValue()); + + cleanupUpstreamAndDownstream(); +} + } // namespace Envoy diff --git a/test/mocks/http/mocks.cc b/test/mocks/http/mocks.cc index 81f6bcd093ff6..3e53e79dd35a9 100644 --- a/test/mocks/http/mocks.cc +++ b/test/mocks/http/mocks.cc @@ -91,6 +91,11 @@ void MockStreamDecoderFilterCallbacks::sendLocalReply_( stream_destroyed_, Utility::EncodeFunctions{ nullptr, nullptr, + [](ResponseHeaderMap& headers, Code& code, std::string& body, + const absl::optional grpc_status, + bool is_head_request) -> void { + Utility::toGrpcTrailersOnlyResponse(headers, code, body, grpc_status, is_head_request); + }, [this, modify_headers, details](ResponseHeaderMapPtr&& headers, bool end_stream) -> void { if (modify_headers != nullptr) { modify_headers(*headers); diff --git a/test/mocks/local_reply/mocks.h b/test/mocks/local_reply/mocks.h index 913f815d50691..52b03d1536a98 100644 --- a/test/mocks/local_reply/mocks.h +++ b/test/mocks/local_reply/mocks.h @@ -9,11 +9,18 @@ class MockLocalReply : public LocalReply { MockLocalReply(); ~MockLocalReply() override; - MOCK_METHOD(void, rewrite, + MOCK_METHOD(bool, rewrite, (const Http::RequestHeaderMap* request_headers, Http::ResponseHeaderMap& response_headers, StreamInfo::StreamInfo& stream_info, Http::Code& code, std::string& body, absl::string_view& content_type), (const)); + + MOCK_METHOD(bool, match, + (const Http::RequestHeaderMap* request_headers, + const Http::ResponseHeaderMap& response_headers, + const Http::ResponseTrailerMap* response_trailers, + StreamInfo::StreamInfo& stream_info), + (const)); }; } // namespace LocalReply } // namespace Envoy diff --git a/test/tools/router_check/router.cc b/test/tools/router_check/router.cc index 73a5d1a0d6334..83f7a39ea9146 100644 --- a/test/tools/router_check/router.cc +++ b/test/tools/router_check/router.cc @@ -170,7 +170,7 @@ void RouterCheckTool::finalizeHeaders(ToolConfig& tool_config, void RouterCheckTool::sendLocalReply(ToolConfig& tool_config, const Router::DirectResponseEntry& entry) { auto encode_functions = Envoy::Http::Utility::EncodeFunctions{ - nullptr, nullptr, + nullptr, nullptr, nullptr, [&](Envoy::Http::ResponseHeaderMapPtr&& headers, bool end_stream) -> void { UNREFERENCED_PARAMETER(end_stream); Http::HeaderMapImpl::copyFrom(*tool_config.response_headers_->header_map_, *headers);