diff --git a/include/envoy/http/filter.h b/include/envoy/http/filter.h index b3015ded6ecf6..32a4087a949b8 100644 --- a/include/envoy/http/filter.h +++ b/include/envoy/http/filter.h @@ -190,6 +190,17 @@ class StreamDecoderFilterCallbacks : public virtual StreamFilterCallbacks { */ virtual void addDecodedData(Buffer::Instance& data, bool streaming_filter) PURE; + /** + * Adds decoded trailers. May only be called in decodeData when end_stream is set to true. + * If called in any other context, an assertion will be triggered. + * + * When called in decodeData, the trailers map will be initialized to an empty map and returned by + * reference. Calling this function more than once is invalid. + * + * @return a reference to the newly created trailers map. + */ + virtual HeaderMap& addDecodedTrailers() PURE; + /** * Create a locally generated response using the provided response_code and body_text parameters. * If the request was a gRPC request the local reply will be encoded as a gRPC response with a 200 @@ -395,6 +406,17 @@ class StreamEncoderFilterCallbacks : public virtual StreamFilterCallbacks { */ virtual void addEncodedData(Buffer::Instance& data, bool streaming_filter) PURE; + /** + * Adds encoded trailers. May only be called in encodeData when end_stream is set to true. + * If called in any other context, an assertion will be triggered. + * + * When called in encodeData, the trailers map will be initialized to an empty map and returned by + * reference. Calling this function more than once is invalid. + * + * @return a reference to the newly created trailers map. + */ + virtual HeaderMap& addEncodedTrailers() PURE; + /** * Called when an encoder filter goes over its high watermark. */ diff --git a/source/common/http/async_client_impl.h b/source/common/http/async_client_impl.h index 4632166133a7d..801479bd43af2 100644 --- a/source/common/http/async_client_impl.h +++ b/source/common/http/async_client_impl.h @@ -268,6 +268,7 @@ class AsyncStreamImpl : public AsyncClient::Stream, Tracing::Span& activeSpan() override { return active_span_; } const Tracing::Config& tracingConfig() override { return tracing_config_; } void continueDecoding() override { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; } + HeaderMap& addDecodedTrailers() override { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; } void addDecodedData(Buffer::Instance&, bool) override { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; } const Buffer::Instance* decodingBuffer() override { return buffered_body_.get(); } void sendLocalReply(Code code, const std::string& body, diff --git a/source/common/http/conn_manager_impl.cc b/source/common/http/conn_manager_impl.cc index 8475ec8c9a704..4c6709473b96f 100644 --- a/source/common/http/conn_manager_impl.cc +++ b/source/common/http/conn_manager_impl.cc @@ -767,6 +767,8 @@ void ConnectionManagerImpl::ActiveStream::decodeData(ActiveStreamDecoderFilter* } std::list::iterator entry; + auto trailers_added_entry = decoder_filters_.end(); + const bool trailers_exists_at_start = request_trailers_ != nullptr; if (!filter) { entry = decoder_filters_.begin(); } else { @@ -775,15 +777,52 @@ void ConnectionManagerImpl::ActiveStream::decodeData(ActiveStreamDecoderFilter* for (; entry != decoder_filters_.end(); entry++) { ASSERT(!(state_.filter_call_state_ & FilterCallState::DecodeData)); + + // We check the request_trailers_ pointer here in case addDecodedTrailers + // is called in decodeData during a previous filter invocation, at which point we communicate to + // the current and future filters that the stream has not yet ended. + if (end_stream) { + state_.filter_call_state_ |= FilterCallState::LastDataFrame; + } state_.filter_call_state_ |= FilterCallState::DecodeData; - FilterDataStatus status = (*entry)->handle_->decodeData(data, end_stream); + FilterDataStatus status = (*entry)->handle_->decodeData(data, end_stream && !request_trailers_); state_.filter_call_state_ &= ~FilterCallState::DecodeData; + if (end_stream) { + state_.filter_call_state_ &= ~FilterCallState::LastDataFrame; + } ENVOY_STREAM_LOG(trace, "decode data called: filter={} status={}", *this, static_cast((*entry).get()), static_cast(status)); - if (!(*entry)->commonHandleAfterDataCallback(status, data, state_.decoder_filters_streaming_)) { + + if (!trailers_exists_at_start && request_trailers_ && + trailers_added_entry == decoder_filters_.end()) { + trailers_added_entry = entry; + } + + if (!(*entry)->commonHandleAfterDataCallback(status, data, state_.decoder_filters_streaming_) && + std::next(entry) != decoder_filters_.end()) { + // Stop iteration IFF this is not the last filter. If it is the last filter, continue with + // processing since we need to handle the case where a terminal filter wants to buffer, but + // a previous filter has added trailers. return; } } + + // If trailers were adding during decodeData we need to trigger decodeTrailers in order + // to allow filters to process the trailers. + if (trailers_added_entry != decoder_filters_.end()) { + decodeTrailers(trailers_added_entry->get(), *request_trailers_); + } +} + +HeaderMap& ConnectionManagerImpl::ActiveStream::addDecodedTrailers() { + // Trailers can only be added during the last data frame (i.e. end_stream = true). + ASSERT(state_.filter_call_state_ & FilterCallState::LastDataFrame); + + // Trailers can only be added once. + ASSERT(!request_trailers_); + + request_trailers_ = std::make_unique(); + return *request_trailers_; } void ConnectionManagerImpl::ActiveStream::addDecodedData(ActiveStreamDecoderFilter& filter, @@ -1058,6 +1097,17 @@ void ConnectionManagerImpl::ActiveStream::encodeHeaders(ActiveStreamEncoderFilte } } +HeaderMap& ConnectionManagerImpl::ActiveStream::addEncodedTrailers() { + // Trailers can only be added during the last data frame (i.e. end_stream = true). + ASSERT(state_.filter_call_state_ & FilterCallState::LastDataFrame); + + // Trailers can only be added once. + ASSERT(!response_trailers_); + + response_trailers_ = std::make_unique(); + return *response_trailers_; +} + void ConnectionManagerImpl::ActiveStream::addEncodedData(ActiveStreamEncoderFilter& filter, Buffer::Instance& data, bool streaming) { if (state_.filter_call_state_ == 0 || @@ -1083,13 +1133,33 @@ void ConnectionManagerImpl::ActiveStream::encodeData(ActiveStreamEncoderFilter* Buffer::Instance& data, bool end_stream) { resetIdleTimer(); std::list::iterator entry = commonEncodePrefix(filter, end_stream); + auto trailers_added_entry = encoder_filters_.end(); + + const bool trailers_exists_at_start = response_trailers_ != nullptr; for (; entry != encoder_filters_.end(); entry++) { ASSERT(!(state_.filter_call_state_ & FilterCallState::EncodeData)); + + // We check the response_trailers_ pointer here in case addEncodedTrailers + // is called in encodeData during a previous filter invocation, at which point we communicate to + // the current and future filters that the stream has not yet ended. state_.filter_call_state_ |= FilterCallState::EncodeData; - FilterDataStatus status = (*entry)->handle_->encodeData(data, end_stream); + if (end_stream) { + state_.filter_call_state_ |= FilterCallState::LastDataFrame; + } + FilterDataStatus status = + (*entry)->handle_->encodeData(data, end_stream && !response_trailers_); state_.filter_call_state_ &= ~FilterCallState::EncodeData; + if (end_stream) { + state_.filter_call_state_ &= ~FilterCallState::LastDataFrame; + } ENVOY_STREAM_LOG(trace, "encode data called: filter={} status={}", *this, static_cast((*entry).get()), static_cast(status)); + + if (!trailers_exists_at_start && response_trailers_ && + trailers_added_entry == encoder_filters_.end()) { + trailers_added_entry = entry; + } + if (!(*entry)->commonHandleAfterDataCallback(status, data, state_.encoder_filters_streaming_)) { return; } @@ -1099,8 +1169,16 @@ void ConnectionManagerImpl::ActiveStream::encodeData(ActiveStreamEncoderFilter* end_stream); request_info_.addBytesSent(data.length()); - response_encoder_->encodeData(data, end_stream); - maybeEndEncode(end_stream); + + // If trailers were adding during encodeData we need to trigger decodeTrailers in order + // to allow filters to process the trailers. + if (trailers_added_entry != encoder_filters_.end()) { + response_encoder_->encodeData(data, false); + encodeTrailers(trailers_added_entry->get(), *response_trailers_); + } else { + response_encoder_->encodeData(data, end_stream); + maybeEndEncode(end_stream); + } } void ConnectionManagerImpl::ActiveStream::encodeTrailers(ActiveStreamEncoderFilter* filter, @@ -1380,6 +1458,10 @@ Buffer::WatermarkBufferPtr ConnectionManagerImpl::ActiveStreamDecoderFilter::cre return buffer; } +HeaderMap& ConnectionManagerImpl::ActiveStreamDecoderFilter::addDecodedTrailers() { + return parent_.addDecodedTrailers(); +} + void ConnectionManagerImpl::ActiveStreamDecoderFilter::addDecodedData(Buffer::Instance& data, bool streaming) { parent_.addDecodedData(*this, data, streaming); @@ -1473,6 +1555,10 @@ void ConnectionManagerImpl::ActiveStreamEncoderFilter::addEncodedData(Buffer::In return parent_.addEncodedData(*this, data, streaming); } +HeaderMap& ConnectionManagerImpl::ActiveStreamEncoderFilter::addEncodedTrailers() { + return parent_.addEncodedTrailers(); +} + void ConnectionManagerImpl::ActiveStreamEncoderFilter:: onEncoderFilterAboveWriteBufferHighWatermark() { ENVOY_STREAM_LOG(debug, "Disabling upstream stream due to filter callbacks.", parent_); diff --git a/source/common/http/conn_manager_impl.h b/source/common/http/conn_manager_impl.h index 672ff52433cb2..5d199f12cc5f5 100644 --- a/source/common/http/conn_manager_impl.h +++ b/source/common/http/conn_manager_impl.h @@ -162,6 +162,7 @@ class ConnectionManagerImpl : Logger::Loggable, // Http::StreamDecoderFilterCallbacks void addDecodedData(Buffer::Instance& data, bool streaming) override; + HeaderMap& addDecodedTrailers() override; void continueDecoding() override; const Buffer::Instance* decodingBuffer() override { return parent_.buffered_request_data_.get(); @@ -229,6 +230,7 @@ class ConnectionManagerImpl : Logger::Loggable, // Http::StreamEncoderFilterCallbacks void addEncodedData(Buffer::Instance& data, bool streaming) override; + HeaderMap& addEncodedTrailers() override; void onEncoderFilterAboveWriteBufferHighWatermark() override; void onEncoderFilterBelowWriteBufferLowWatermark() override; void setEncoderBufferLimit(uint32_t limit) override { parent_.setBufferLimit(limit); } @@ -267,11 +269,13 @@ class ConnectionManagerImpl : Logger::Loggable, commonEncodePrefix(ActiveStreamEncoderFilter* filter, bool end_stream); const Network::Connection* connection(); void addDecodedData(ActiveStreamDecoderFilter& filter, Buffer::Instance& data, bool streaming); + HeaderMap& addDecodedTrailers(); void decodeHeaders(ActiveStreamDecoderFilter* filter, HeaderMap& headers, bool end_stream); void decodeData(ActiveStreamDecoderFilter* filter, Buffer::Instance& data, bool end_stream); void decodeTrailers(ActiveStreamDecoderFilter* filter, HeaderMap& trailers); void maybeEndDecode(bool end_stream); void addEncodedData(ActiveStreamEncoderFilter& filter, Buffer::Instance& data, bool streaming); + HeaderMap& addEncodedTrailers(); void sendLocalReply(bool is_grpc_request, Code code, const std::string& body, std::function modify_headers); void encode100ContinueHeaders(ActiveStreamEncoderFilter* filter, HeaderMap& headers); @@ -339,6 +343,9 @@ class ConnectionManagerImpl : Logger::Loggable, // to verify we do not encode100Continue headers more than once per // filter. static constexpr uint32_t Encode100ContinueHeaders = 0x40; + // Used to indicate that we're processing the final [En|De]codeData frame, + // i.e. end_stream = true + static constexpr uint32_t LastDataFrame = 0x80; }; // clang-format on diff --git a/test/common/http/conn_manager_impl_test.cc b/test/common/http/conn_manager_impl_test.cc index 1889dbd09e45c..6a468ff0cad87 100644 --- a/test/common/http/conn_manager_impl_test.cc +++ b/test/common/http/conn_manager_impl_test.cc @@ -2271,6 +2271,173 @@ TEST_F(HttpConnectionManagerImplTest, ZeroByteDataFiltering) { decoder_filters_[0]->callbacks_->continueDecoding(); } +TEST_F(HttpConnectionManagerImplTest, FilterAddTrailersInTrailersCallback) { + InSequence s; + setup(false, ""); + + EXPECT_CALL(*codec_, dispatch(_)).WillOnce(Invoke([&](Buffer::Instance&) -> void { + StreamDecoder* decoder = &conn_manager_->newStream(response_encoder_); + HeaderMapPtr headers{new TestHeaderMapImpl{{":authority", "host"}, {":path", "/"}}}; + decoder->decodeHeaders(std::move(headers), false); + + Buffer::OwnedImpl fake_data("hello"); + decoder->decodeData(fake_data, false); + + HeaderMapPtr trailers{new TestHeaderMapImpl{{"bazzz", "bar"}}}; + decoder->decodeTrailers(std::move(trailers)); + })); + + setupFilterChain(2, 2); + + Http::LowerCaseString trailer_key("foo"); + std::string trailers_data("trailers"); + EXPECT_CALL(*decoder_filters_[0], decodeHeaders(_, false)) + .WillOnce(Return(FilterHeadersStatus::StopIteration)); + EXPECT_CALL(*decoder_filters_[0], decodeData(_, false)) + .WillOnce(Return(FilterDataStatus::StopIterationAndBuffer)); + EXPECT_CALL(*decoder_filters_[0], decodeTrailers(_)) + .WillOnce(Return(FilterTrailersStatus::Continue)); + EXPECT_CALL(*decoder_filters_[1], decodeHeaders(_, false)) + .WillOnce(Return(FilterHeadersStatus::StopIteration)); + EXPECT_CALL(*decoder_filters_[1], decodeData(_, false)) + .WillOnce(Return(FilterDataStatus::StopIterationAndBuffer)); + EXPECT_CALL(*decoder_filters_[1], decodeTrailers(_)) + .WillOnce(Invoke([&](Http::HeaderMap& trailers) -> FilterTrailersStatus { + Http::LowerCaseString key("foo"); + EXPECT_EQ(trailers.get(key), nullptr); + return FilterTrailersStatus::Continue; + })); + + // Kick off the incoming data. + Buffer::OwnedImpl fake_input("1234"); + conn_manager_->onData(fake_input, false); + + // set up encodeHeaders expectations + EXPECT_CALL(*encoder_filters_[0], encodeHeaders(_, false)) + .WillOnce(Return(FilterHeadersStatus::Continue)); + EXPECT_CALL(*encoder_filters_[1], encodeHeaders(_, false)) + .WillOnce(Return(FilterHeadersStatus::Continue)); + EXPECT_CALL(response_encoder_, encodeHeaders(_, false)); + + // invoke encodeHeaders + decoder_filters_[0]->callbacks_->encodeHeaders( + HeaderMapPtr{new TestHeaderMapImpl{{":status", "200"}}}, false); + + // set up encodeData expectations + EXPECT_CALL(*encoder_filters_[0], encodeData(_, false)) + .WillOnce(Return(FilterDataStatus::Continue)); + EXPECT_CALL(*encoder_filters_[1], encodeData(_, false)) + .WillOnce(Return(FilterDataStatus::Continue)); + EXPECT_CALL(response_encoder_, encodeData(_, false)); + + // invoke encodeData + Buffer::OwnedImpl response_body("response"); + decoder_filters_[0]->callbacks_->encodeData(response_body, false); + // set up encodeTrailer expectations + EXPECT_CALL(*encoder_filters_[0], encodeTrailers(_)) + .WillOnce(Return(FilterTrailersStatus::Continue)); + + EXPECT_CALL(*encoder_filters_[1], encodeTrailers(_)) + .WillOnce(Invoke([&](Http::HeaderMap& trailers) -> FilterTrailersStatus { + // assert that the trailers set in the previous filter was ignored + Http::LowerCaseString key("foo"); + EXPECT_EQ(trailers.get(key), nullptr); + return FilterTrailersStatus::Continue; + })); + EXPECT_CALL(response_encoder_, encodeTrailers(_)); + expectOnDestroy(); + + // invoke encodeTrailers + decoder_filters_[0]->callbacks_->encodeTrailers( + HeaderMapPtr{new TestHeaderMapImpl{{"some", "trailer"}}}); +} + +TEST_F(HttpConnectionManagerImplTest, FilterAddTrailersInDataCallbackNoTrailers) { + InSequence s; + setup(false, ""); + + EXPECT_CALL(*codec_, dispatch(_)).WillOnce(Invoke([&](Buffer::Instance&) -> void { + StreamDecoder* decoder = &conn_manager_->newStream(response_encoder_); + HeaderMapPtr headers{new TestHeaderMapImpl{{":authority", "host"}, {":path", "/"}}}; + decoder->decodeHeaders(std::move(headers), false); + + Buffer::OwnedImpl fake_data("hello"); + decoder->decodeData(fake_data, true); + })); + + setupFilterChain(2, 2); + + std::string trailers_data("trailers"); + Http::LowerCaseString trailer_key("foo"); + EXPECT_CALL(*decoder_filters_[0], decodeHeaders(_, false)) + .WillOnce(Return(FilterHeadersStatus::Continue)); + EXPECT_CALL(*decoder_filters_[1], decodeHeaders(_, false)) + .WillOnce(Return(FilterHeadersStatus::Continue)); + EXPECT_CALL(*decoder_filters_[0], decodeData(_, true)) + .WillOnce(InvokeWithoutArgs([&]() -> FilterDataStatus { + decoder_filters_[0]->callbacks_->addDecodedTrailers().addCopy(trailer_key, trailers_data); + return FilterDataStatus::Continue; + })); + + // ensure that the second decodeData call sees end_stream = false + EXPECT_CALL(*decoder_filters_[1], decodeData(_, false)) + .WillOnce(Return(FilterDataStatus::Continue)); + + // since we added trailers, we should see decodeTrailers + EXPECT_CALL(*decoder_filters_[1], decodeTrailers(_)).WillOnce(Invoke([&](HeaderMap& trailers) { + // ensure that we see the trailers set in decodeData + Http::LowerCaseString key("foo"); + auto t = trailers.get(key); + ASSERT(t); + EXPECT_EQ(t->value(), trailers_data.c_str()); + return FilterTrailersStatus::Continue; + })); + + // Kick off the incoming data. + Buffer::OwnedImpl fake_input("1234"); + conn_manager_->onData(fake_input, false); + + // set up encodeHeaders expectations + EXPECT_CALL(*encoder_filters_[0], encodeHeaders(_, false)) + .WillOnce(Return(FilterHeadersStatus::Continue)); + EXPECT_CALL(*encoder_filters_[1], encodeHeaders(_, false)) + .WillOnce(Return(FilterHeadersStatus::Continue)); + EXPECT_CALL(response_encoder_, encodeHeaders(_, false)); + + // invoke encodeHeaders + decoder_filters_[0]->callbacks_->encodeHeaders( + HeaderMapPtr{new TestHeaderMapImpl{{":status", "200"}}}, false); + + // set up encodeData expectations + EXPECT_CALL(*encoder_filters_[0], encodeData(_, true)) + .WillOnce(InvokeWithoutArgs([&]() -> FilterDataStatus { + encoder_filters_[0]->callbacks_->addEncodedTrailers().addCopy(trailer_key, trailers_data); + return FilterDataStatus::Continue; + })); + // ensure encodeData calls after setting header sees end_stream = false + EXPECT_CALL(*encoder_filters_[1], encodeData(_, false)) + .WillOnce(Return(FilterDataStatus::Continue)); + + EXPECT_CALL(response_encoder_, encodeData(_, false)); + + // since we added trailers, we should see encodeTrailer callbacks + EXPECT_CALL(*encoder_filters_[1], encodeTrailers(_)).WillOnce(Invoke([&](HeaderMap& trailers) { + // ensure that we see the trailers set in decodeData + Http::LowerCaseString key("foo"); + auto t = trailers.get(key); + EXPECT_EQ(t->value(), trailers_data.c_str()); + return FilterTrailersStatus::Continue; + })); + + // Ensure that we call encodeTrailers + EXPECT_CALL(response_encoder_, encodeTrailers(_)); + + expectOnDestroy(); + // invoke encodeData + Buffer::OwnedImpl response_body("response"); + decoder_filters_[0]->callbacks_->encodeData(response_body, true); +} + TEST_F(HttpConnectionManagerImplTest, FilterAddBodyInTrailersCallback) { InSequence s; setup(false, ""); diff --git a/test/integration/BUILD b/test/integration/BUILD index 034d9682e0841..3285733c2b4a5 100644 --- a/test/integration/BUILD +++ b/test/integration/BUILD @@ -191,6 +191,22 @@ envoy_cc_test( ], ) +envoy_cc_test_library( + name = "add_trailers_filter_config_lib", + srcs = [ + "add_trailers_filter.cc", + "add_trailers_filter.h", + "add_trailers_filter_config.cc", + "add_trailers_filter_config.h", + ], + deps = [ + "//include/envoy/http:filter_interface", + "//include/envoy/registry", + "//include/envoy/server:filter_config_interface", + "//source/extensions/filters/http/common:empty_http_filter_config_lib", + ], +) + envoy_cc_test_library( name = "http_integration_lib", srcs = [ @@ -200,6 +216,7 @@ envoy_cc_test_library( "http_integration.h", ], deps = [ + ":add_trailers_filter_config_lib", ":integration_lib", "//source/extensions/filters/http/router:config", "//source/extensions/filters/network/http_connection_manager:config", diff --git a/test/integration/add_trailers_filter.cc b/test/integration/add_trailers_filter.cc new file mode 100644 index 0000000000000..d9bfde74668ab --- /dev/null +++ b/test/integration/add_trailers_filter.cc @@ -0,0 +1,21 @@ +#include "test/integration/add_trailers_filter.h" + +#include + +namespace Envoy { +Http::FilterDataStatus AddTrailersStreamFilter::decodeData(Buffer::Instance&, bool end_stream) { + if (end_stream) { + decoder_callbacks_->addDecodedTrailers().insertGrpcMessage().value(std::string("decode")); + } + + return Http::FilterDataStatus::Continue; +} + +Http::FilterDataStatus AddTrailersStreamFilter::encodeData(Buffer::Instance&, bool end_stream) { + if (end_stream) { + encoder_callbacks_->addEncodedTrailers().insertGrpcMessage().value(std::string("encode")); + } + + return Http::FilterDataStatus::Continue; +} +} // namespace Envoy diff --git a/test/integration/add_trailers_filter.h b/test/integration/add_trailers_filter.h new file mode 100644 index 0000000000000..f86fdc15517db --- /dev/null +++ b/test/integration/add_trailers_filter.h @@ -0,0 +1,44 @@ +#pragma once + +#include "envoy/http/filter.h" + +namespace Envoy { +// a test filter that inserts trailers at the end of encode/decode +class AddTrailersStreamFilter : public Http::StreamFilter { +public: + // Http::StreamFilterBase + void onDestroy() override {} + + // Http::StreamDecoderFilter + Http::FilterHeadersStatus decodeHeaders(Http::HeaderMap&, bool) override { + return Http::FilterHeadersStatus::Continue; + } + Http::FilterDataStatus decodeData(Buffer::Instance&, bool end_stream) override; + + Http::FilterTrailersStatus decodeTrailers(Http::HeaderMap&) override { + return Http::FilterTrailersStatus::Continue; + } + void setDecoderFilterCallbacks(Http::StreamDecoderFilterCallbacks& callbacks) override { + decoder_callbacks_ = &callbacks; + } + + // Http::StreamEncoderFilter + Http::FilterHeadersStatus encode100ContinueHeaders(Http::HeaderMap&) override { + return Http::FilterHeadersStatus::Continue; + } + Http::FilterHeadersStatus encodeHeaders(Http::HeaderMap&, bool) override { + return Http::FilterHeadersStatus::Continue; + } + Http::FilterDataStatus encodeData(Buffer::Instance&, bool end_stream) override; + Http::FilterTrailersStatus encodeTrailers(Http::HeaderMap&) override { + return Http::FilterTrailersStatus::Continue; + } + void setEncoderFilterCallbacks(Http::StreamEncoderFilterCallbacks& callbacks) override { + encoder_callbacks_ = &callbacks; + } + +private: + Http::StreamDecoderFilterCallbacks* decoder_callbacks_{}; + Http::StreamEncoderFilterCallbacks* encoder_callbacks_{}; +}; +} // namespace Envoy diff --git a/test/integration/add_trailers_filter_config.cc b/test/integration/add_trailers_filter_config.cc new file mode 100644 index 0000000000000..2aad49f9a55c3 --- /dev/null +++ b/test/integration/add_trailers_filter_config.cc @@ -0,0 +1,20 @@ +#include "test/integration/add_trailers_filter_config.h" + +#include "envoy/registry/registry.h" + +#include "test/integration/add_trailers_filter.h" + +namespace Envoy { +Http::FilterFactoryCb +AddTrailersStreamFilterConfig::createFilter(const std::string&, + Server::Configuration::FactoryContext&) { + return [](Http::FilterChainFactoryCallbacks& callbacks) -> void { + callbacks.addStreamFilter(std::make_shared<::Envoy::AddTrailersStreamFilter>()); + }; +} + +// perform static registration +static Registry::RegisterFactory + register_; +} // namespace Envoy diff --git a/test/integration/add_trailers_filter_config.h b/test/integration/add_trailers_filter_config.h new file mode 100644 index 0000000000000..8f606ee68e2f7 --- /dev/null +++ b/test/integration/add_trailers_filter_config.h @@ -0,0 +1,18 @@ +#pragma once + +#include "envoy/registry/registry.h" +#include "envoy/server/filter_config.h" + +#include "extensions/filters/http/common/empty_http_filter_config.h" + +#include "test/integration/add_trailers_filter.h" + +namespace Envoy { +class AddTrailersStreamFilterConfig + : public Extensions::HttpFilters::Common::EmptyHttpFilterConfig { +public: + AddTrailersStreamFilterConfig() : EmptyHttpFilterConfig("add-trailers-filter") {} + + Http::FilterFactoryCb createFilter(const std::string&, Server::Configuration::FactoryContext&); +}; +} // namespace Envoy diff --git a/test/integration/http2_integration_test.cc b/test/integration/http2_integration_test.cc index ae04ba0db8291..37bc3a3368ac5 100644 --- a/test/integration/http2_integration_test.cc +++ b/test/integration/http2_integration_test.cc @@ -38,6 +38,8 @@ TEST_P(Http2IntegrationTest, MultipleContentLengths) { testMultipleContentLength TEST_P(Http2IntegrationTest, ComputedHealthCheck) { testComputedHealthCheck(); } +TEST_P(Http2IntegrationTest, AddEncodedTrailers) { testAddEncodedTrailers(); } + TEST_P(Http2IntegrationTest, DrainClose) { testDrainClose(); } TEST_P(Http2IntegrationTest, RouterRequestAndResponseWithBodyNoBuffer) { diff --git a/test/integration/http2_upstream_integration_test.cc b/test/integration/http2_upstream_integration_test.cc index 2d4b3be30a52e..1275b32199f23 100644 --- a/test/integration/http2_upstream_integration_test.cc +++ b/test/integration/http2_upstream_integration_test.cc @@ -22,6 +22,8 @@ TEST_P(Http2UpstreamIntegrationTest, RouterRedirect) { testRouterRedirect(); } TEST_P(Http2UpstreamIntegrationTest, ComputedHealthCheck) { testComputedHealthCheck(); } +TEST_P(Http2UpstreamIntegrationTest, AddEncodedTrailers) { testAddEncodedTrailers(); } + TEST_P(Http2UpstreamIntegrationTest, DrainClose) { testDrainClose(); } TEST_P(Http2UpstreamIntegrationTest, RouterRequestAndResponseWithBodyNoBuffer) { diff --git a/test/integration/http_integration.cc b/test/integration/http_integration.cc index 5c3204dbdf8dc..daf08871cb612 100644 --- a/test/integration/http_integration.cc +++ b/test/integration/http_integration.cc @@ -418,6 +418,35 @@ name: envoy.health_check EXPECT_STREQ("503", response->headers().Status()->value().c_str()); } +void HttpIntegrationTest::testAddEncodedTrailers() { + config_helper_.addFilter(R"EOF( +name: add-trailers-filter +config: {} +)EOF"); + initialize(); + + codec_client_ = makeHttpConnection(lookupPort("http")); + auto response = + codec_client_->makeRequestWithBody(Http::TestHeaderMapImpl{{":method", "GET"}, + {":path", "/test/long/url"}, + {":scheme", "http"}, + {":authority", "host"}}, + 128); + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders(Http::TestHeaderMapImpl{{":status", "503"}}, false); + upstream_request_->encodeData(128, true); + response->waitForEndStream(); + + if (upstreamProtocol() == FakeHttpConnection::Type::HTTP2) { + EXPECT_STREQ("decode", upstream_request_->trailers()->GrpcMessage()->value().c_str()); + } + EXPECT_TRUE(response->complete()); + EXPECT_STREQ("503", response->headers().Status()->value().c_str()); + if (downstream_protocol_ == Http::CodecClient::Type::HTTP2) { + EXPECT_STREQ("encode", response->trailers()->GrpcMessage()->value().c_str()); + } +} + // Add a health check filter and verify correct behavior when draining. void HttpIntegrationTest::testDrainClose() { config_helper_.addFilter(ConfigHelper::DEFAULT_HEALTH_CHECK_FILTER); diff --git a/test/integration/http_integration.h b/test/integration/http_integration.h index 1b6c3b760157c..57cbc6f7df040 100644 --- a/test/integration/http_integration.h +++ b/test/integration/http_integration.h @@ -156,6 +156,7 @@ class HttpIntegrationTest : public BaseIntegrationTest { void testInvalidContentLength(); void testMultipleContentLengths(); void testComputedHealthCheck(); + void testAddEncodedTrailers(); void testDrainClose(); void testRetry(); void testRetryHittingBufferLimit(); diff --git a/test/integration/integration_test.cc b/test/integration/integration_test.cc index e4fa9feb4ae38..19504c9c7fb02 100644 --- a/test/integration/integration_test.cc +++ b/test/integration/integration_test.cc @@ -40,6 +40,8 @@ TEST_P(IntegrationTest, RouterDirectResponse) { testRouterDirectResponse(); } TEST_P(IntegrationTest, ComputedHealthCheck) { testComputedHealthCheck(); } +TEST_P(IntegrationTest, AddEncodedTrailers) { testAddEncodedTrailers(); } + TEST_P(IntegrationTest, DrainClose) { testDrainClose(); } TEST_P(IntegrationTest, ConnectionClose) { diff --git a/test/mocks/http/mocks.h b/test/mocks/http/mocks.h index 233232a829f4a..a6cbf6dd18b39 100644 --- a/test/mocks/http/mocks.h +++ b/test/mocks/http/mocks.h @@ -222,6 +222,7 @@ class MockStreamDecoderFilterCallbacks : public StreamDecoderFilterCallbacks, MOCK_METHOD0(continueDecoding, void()); MOCK_METHOD2(addDecodedData, void(Buffer::Instance& data, bool streaming)); + MOCK_METHOD0(addDecodedTrailers, HeaderMap&()); MOCK_METHOD0(decodingBuffer, const Buffer::Instance*()); MOCK_METHOD1(encode100ContinueHeaders_, void(HeaderMap& headers)); MOCK_METHOD2(encodeHeaders_, void(HeaderMap& headers, bool end_stream)); @@ -259,6 +260,7 @@ class MockStreamEncoderFilterCallbacks : public StreamEncoderFilterCallbacks, // Http::StreamEncoderFilterCallbacks MOCK_METHOD2(addEncodedData, void(Buffer::Instance& data, bool streaming)); + MOCK_METHOD0(addEncodedTrailers, HeaderMap&()); MOCK_METHOD0(continueEncoding, void()); MOCK_METHOD0(encodingBuffer, const Buffer::Instance*());