diff --git a/api/envoy/config/filter/network/http_connection_manager/v2/http_connection_manager.proto b/api/envoy/config/filter/network/http_connection_manager/v2/http_connection_manager.proto index 19f3fe3dd37d..7f4ea0c73f7c 100644 --- a/api/envoy/config/filter/network/http_connection_manager/v2/http_connection_manager.proto +++ b/api/envoy/config/filter/network/http_connection_manager/v2/http_connection_manager.proto @@ -300,6 +300,14 @@ message HttpConnectionManager { // is terminated with a 408 Request Timeout error code if no upstream response // header has been received, otherwise a stream reset occurs. // + // This timeout also specifies the amount of time that Envoy will wait for the peer to open enough + // window to write any remaining stream data once the entirety of stream data (local end stream is + // true) has been buffered pending available window. In other words, this timeout defends against + // a peer that does not release enough window to completely write the stream, even though all + // data has been proxied within available flow control windows. If the timeout is hit in this + // case, the :ref:`tx_flush_timeout ` counter will be + // incremented. + // // Note that it is possible to idle timeout even if the wire traffic for a stream is non-idle, due // to the granularity of events presented to the connection manager. For example, while receiving // very large request headers, it may be the case that there is traffic regularly arriving on the diff --git a/api/envoy/config/filter/network/http_connection_manager/v3alpha/http_connection_manager.proto b/api/envoy/config/filter/network/http_connection_manager/v3alpha/http_connection_manager.proto index f96b590d7130..040895af4594 100644 --- a/api/envoy/config/filter/network/http_connection_manager/v3alpha/http_connection_manager.proto +++ b/api/envoy/config/filter/network/http_connection_manager/v3alpha/http_connection_manager.proto @@ -287,6 +287,14 @@ message HttpConnectionManager { // is terminated with a 408 Request Timeout error code if no upstream response // header has been received, otherwise a stream reset occurs. // + // This timeout also specifies the amount of time that Envoy will wait for the peer to open enough + // window to write any remaining stream data once the entirety of stream data (local end stream is + // true) has been buffered pending available window. In other words, this timeout defends against + // a peer that does not release enough window to completely write the stream, even though all + // data has been proxied within available flow control windows. If the timeout is hit in this + // case, the :ref:`tx_flush_timeout ` counter will be + // incremented. + // // Note that it is possible to idle timeout even if the wire traffic for a stream is non-idle, due // to the granularity of events presented to the connection manager. For example, while receiving // very large request headers, it may be the case that there is traffic regularly arriving on the diff --git a/docs/root/configuration/http/http_conn_man/stats.rst b/docs/root/configuration/http/http_conn_man/stats.rst index 74ce491f39a6..f375832b8c6c 100644 --- a/docs/root/configuration/http/http_conn_man/stats.rst +++ b/docs/root/configuration/http/http_conn_man/stats.rst @@ -137,7 +137,16 @@ All http2 statistics are rooted at *http2.* rx_reset, Counter, Total number of reset stream frames received by Envoy too_many_header_frames, Counter, Total number of times an HTTP2 connection is reset due to receiving too many headers frames. Envoy currently supports proxying at most one header frame for 100-Continue one non-100 response code header frame and one frame with trailers trailers, Counter, Total number of trailers seen on requests coming from downstream + tx_flush_timeout, Counter, Total number of :ref:`stream idle timeouts ` waiting for open stream window to flush the remainder of a stream tx_reset, Counter, Total number of reset stream frames transmitted by Envoy + streams_active, Gauge, Active streams as observed by the codec + pending_send_bytes, Gauge, Currently buffered body data in bytes waiting to be written when stream/connection window is opened. + +.. attention:: + + The HTTP/2 `streams_active` gauge may be greater than the HTTP connection manager + `downstream_rq_active` gauge due to differences in stream accounting between the codec and the + HTTP connection manager. Tracing statistics ------------------ diff --git a/docs/root/faq/configuration/timeouts.rst b/docs/root/faq/configuration/timeouts.rst index 46d927acf299..955037607f34 100644 --- a/docs/root/faq/configuration/timeouts.rst +++ b/docs/root/faq/configuration/timeouts.rst @@ -52,7 +52,9 @@ context request/stream is interchangeable. ` is the amount of time that the connection manager will allow a stream to exist with no upstream or downstream activity. The default stream idle timeout is *5 minutes*. This timeout is strongly - recommended for streaming APIs (requests or responses that never end). + recommended for all requests (not just streaming requests/responses) as it additionally defends + against an HTTP/2 peer that does not open stream window once an entire response has been buffered + to be sent to a downstream client). Route timeouts ^^^^^^^^^^^^^^ diff --git a/docs/root/intro/version_history.rst b/docs/root/intro/version_history.rst index 3faf0cdff3e0..f37428a46186 100644 --- a/docs/root/intro/version_history.rst +++ b/docs/root/intro/version_history.rst @@ -3,6 +3,8 @@ Version history 1.12.5 (Pending) ================ +* http: the :ref:`stream_idle_timeout ` + now also defends against an HTTP/2 peer that does not open stream window once an entire response has been buffered to be sent to a downstream client. * listener: add runtime support for `per-listener limits ` on active/accepted connections. * overload management: add runtime support for :ref:`global limits ` on active/accepted connections. diff --git a/include/envoy/http/codec.h b/include/envoy/http/codec.h index 592635788c37..5e4d55423d7d 100644 --- a/include/envoy/http/codec.h +++ b/include/envoy/http/codec.h @@ -203,6 +203,13 @@ class Stream { * @return uint32_t the stream's configured buffer limits. */ virtual uint32_t bufferLimit() PURE; + + /** + * Set the flush timeout for the stream. At the codec level this is used to bound the amount of + * time the codec will wait to flush body data pending open stream window. It does *not* count + * small window updates as satisfying the idle timeout as this is a potential DoS vector. + */ + virtual void setFlushTimeout(std::chrono::milliseconds timeout) PURE; }; /** diff --git a/source/common/http/codec_client.cc b/source/common/http/codec_client.cc index c6c3c42f3db3..d850acb3c0af 100644 --- a/source/common/http/codec_client.cc +++ b/source/common/http/codec_client.cc @@ -17,7 +17,7 @@ namespace Http { CodecClient::CodecClient(Type type, Network::ClientConnectionPtr&& connection, Upstream::HostDescriptionConstSharedPtr host, Event::Dispatcher& dispatcher) - : type_(type), connection_(std::move(connection)), host_(host), + : type_(type), host_(host), connection_(std::move(connection)), idle_timeout_(host_->cluster().idleTimeout()) { if (type_ != Type::HTTP3) { // Make sure upstream connections process data and then the FIN, rather than processing diff --git a/source/common/http/codec_client.h b/source/common/http/codec_client.h index ab29c15db85a..49ae4bc17a45 100644 --- a/source/common/http/codec_client.h +++ b/source/common/http/codec_client.h @@ -155,9 +155,11 @@ class CodecClient : Logger::Loggable, } const Type type_; - ClientConnectionPtr codec_; - Network::ClientConnectionPtr connection_; + // The order of host_, connection_, and codec_ matter as during destruction each can refer to + // the previous, at least in tests. Upstream::HostDescriptionConstSharedPtr host_; + Network::ClientConnectionPtr connection_; + ClientConnectionPtr codec_; Event::TimerPtr idle_timer_; const absl::optional idle_timeout_; diff --git a/source/common/http/conn_manager_impl.cc b/source/common/http/conn_manager_impl.cc index aa7de6f07fed..90866fb2fb3a 100644 --- a/source/common/http/conn_manager_impl.cc +++ b/source/common/http/conn_manager_impl.cc @@ -261,6 +261,7 @@ StreamDecoder& ConnectionManagerImpl::newStream(StreamEncoder& response_encoder, new_stream->state_.is_internally_created_ = is_internally_created; new_stream->response_encoder_ = &response_encoder; new_stream->response_encoder_->getStream().addCallbacks(*new_stream); + new_stream->response_encoder_->getStream().setFlushTimeout(new_stream->idle_timeout_ms_); new_stream->buffer_limit_ = new_stream->response_encoder_->getStream().bufferLimit(); // If the network connection is backed up, the stream should be made aware of it on creation. // Both HTTP/1.x and HTTP/2 codecs handle this in StreamCallbackHelper::addCallbacks_. @@ -866,7 +867,10 @@ void ConnectionManagerImpl::ActiveStream::decodeHeaders(HeaderMapPtr&& headers, if (hasCachedRoute()) { const Router::RouteEntry* route_entry = cached_route_.value()->routeEntry(); if (route_entry != nullptr && route_entry->idleTimeout()) { + // TODO(mattklein123): Technically if the cached route changes, we should also see if the + // route idle timeout has changed and update the value. idle_timeout_ms_ = route_entry->idleTimeout().value(); + response_encoder_->getStream().setFlushTimeout(idle_timeout_ms_); if (idle_timeout_ms_.count()) { // If we have a route-level idle timeout but no global stream idle timeout, create a timer. if (stream_idle_timer_ == nullptr) { diff --git a/source/common/http/http1/codec_impl.h b/source/common/http/http1/codec_impl.h index 8efd80a4f803..0c85f48c9294 100644 --- a/source/common/http/http1/codec_impl.h +++ b/source/common/http/http1/codec_impl.h @@ -65,6 +65,11 @@ class StreamEncoderImpl : public StreamEncoder, void resetStream(StreamResetReason reason) override; void readDisable(bool disable) override; uint32_t bufferLimit() override; + void setFlushTimeout(std::chrono::milliseconds) override { + // HTTP/1 has one stream per connection, thus any data encoded is immediately written to the + // connection, invoking any watermarks as necessary. There is no internal buffering that would + // require a flush timeout not already covered by other timeouts. + } void isResponseToHeadRequest(bool value) { is_response_to_head_request_ = value; } diff --git a/source/common/http/http2/codec_impl.cc b/source/common/http/http2/codec_impl.cc index b5f38ad7316b..b854f4230727 100644 --- a/source/common/http/http2/codec_impl.cc +++ b/source/common/http/http2/codec_impl.cc @@ -56,11 +56,25 @@ ConnectionImpl::StreamImpl::StreamImpl(ConnectionImpl& parent, uint32_t buffer_l waiting_for_non_informational_headers_(false), pending_receive_buffer_high_watermark_called_(false), pending_send_buffer_high_watermark_called_(false), reset_due_to_messaging_error_(false) { + parent_.stats_.streams_active_.inc(); if (buffer_limit > 0) { setWriteBufferWatermarks(buffer_limit / 2, buffer_limit); } } +ConnectionImpl::StreamImpl::~StreamImpl() { ASSERT(stream_idle_timer_ == nullptr); } + +void ConnectionImpl::StreamImpl::destroy() { + if (stream_idle_timer_ != nullptr) { + // To ease testing and the destructor assertion. + stream_idle_timer_->disableTimer(); + stream_idle_timer_.reset(); + } + + parent_.stats_.streams_active_.dec(); + parent_.stats_.pending_send_bytes_.sub(pending_send_data_.length()); +} + static void insertHeader(std::vector& headers, const HeaderEntry& header) { uint8_t flags = 0; if (header.key().type() == HeaderString::Type::Reference) { @@ -130,6 +144,7 @@ void ConnectionImpl::StreamImpl::encodeTrailers(const HeaderMap& trailers) { // waiting on window updates. We need to save the trailers so that we can emit them later. ASSERT(!pending_trailers_); pending_trailers_ = std::make_unique(trailers); + createPendingFlushTimer(); } else { submitTrailers(trailers); parent_.sendPendingFrames(); @@ -262,6 +277,7 @@ int ConnectionImpl::StreamImpl::onDataSourceSend(const uint8_t* framehd, size_t return NGHTTP2_ERR_FLOODED; } + parent_.stats_.pending_send_bytes_.sub(length); output.move(pending_send_data_, length); parent_.connection_.write(output, false); return 0; @@ -283,9 +299,30 @@ void ConnectionImpl::ServerStreamImpl::submitHeaders(const std::vector 0) { + stream_idle_timer_ = + parent_.connection_.dispatcher().createTimer([this] { onPendingFlushTimer(); }); + stream_idle_timer_->enableTimer(stream_idle_timeout_); + } +} + +void ConnectionImpl::StreamImpl::onPendingFlushTimer() { + ENVOY_CONN_LOG(debug, "pending stream flush timeout", parent_.connection_); + stream_idle_timer_.reset(); + parent_.stats_.tx_flush_timeout_.inc(); + ASSERT(local_end_stream_ && !local_end_stream_sent_); + // This will emit a reset frame for this stream and close the stream locally. No reset callbacks + // will be run because higher layers think the stream is already finished. + resetStreamWorker(StreamResetReason::LocalReset); + parent_.sendPendingFrames(); +} + void ConnectionImpl::StreamImpl::encodeData(Buffer::Instance& data, bool end_stream) { ASSERT(!local_end_stream_); local_end_stream_ = end_stream; + parent_.stats_.pending_send_bytes_.add(data.length()); pending_send_data_.move(data); if (data_deferred_) { int rc = nghttp2_session_resume_data(parent_.session_, stream_id_); @@ -295,6 +332,9 @@ void ConnectionImpl::StreamImpl::encodeData(Buffer::Instance& data, bool end_str } parent_.sendPendingFrames(); + if (local_end_stream_ && pending_send_data_.length() > 0) { + createPendingFlushTimer(); + } } void ConnectionImpl::StreamImpl::resetStream(StreamResetReason reason) { @@ -373,8 +413,10 @@ bool checkRuntimeOverride(bool config_value, const char* override_key) { ConnectionImpl::ConnectionImpl(Network::Connection& connection, Stats::Scope& stats, const Http2Settings& http2_settings, const uint32_t max_headers_kb, const uint32_t max_headers_count) - : stats_{ALL_HTTP2_CODEC_STATS(POOL_COUNTER_PREFIX(stats, "http2."))}, connection_(connection), - max_headers_kb_(max_headers_kb), max_headers_count_(max_headers_count), + : stats_{ALL_HTTP2_CODEC_STATS(POOL_COUNTER_PREFIX(stats, "http2."), + POOL_GAUGE_PREFIX(stats, "http2."))}, + connection_(connection), max_headers_kb_(max_headers_kb), + max_headers_count_(max_headers_count), per_stream_buffer_limit_(http2_settings.initial_stream_window_size_), stream_error_on_invalid_http_messaging_(checkRuntimeOverride( http2_settings.stream_error_on_invalid_http_messaging_, InvalidHttpMessagingOverrideKey)), @@ -396,7 +438,12 @@ ConnectionImpl::ConnectionImpl(Network::Connection& connection, Stats::Scope& st http2_settings.max_inbound_window_update_frames_per_data_frame_sent_)), dispatching_(false), raised_goaway_(false), pending_deferred_reset_(false) {} -ConnectionImpl::~ConnectionImpl() { nghttp2_session_del(session_); } +ConnectionImpl::~ConnectionImpl() { + for (const auto& stream : active_streams_) { + stream->destroy(); + } + nghttp2_session_del(session_); +} void ConnectionImpl::dispatch(Buffer::Instance& data) { ENVOY_CONN_LOG(trace, "dispatching {} bytes", connection_, data.length()); @@ -750,6 +797,7 @@ int ConnectionImpl::onStreamClose(int32_t stream_id, uint32_t error_code) { stream->runResetCallbacks(reason); } + stream->destroy(); connection_.dispatcher().deferredDelete(stream->removeFromList(active_streams_)); // Any unconsumed data must be consumed before the stream is deleted. // nghttp2 does not appear to track this internally, and any stream deleted diff --git a/source/common/http/http2/codec_impl.h b/source/common/http/http2/codec_impl.h index 2abd966f066c..479873d12822 100644 --- a/source/common/http/http2/codec_impl.h +++ b/source/common/http/http2/codec_impl.h @@ -40,7 +40,7 @@ const std::string CLIENT_MAGIC_PREFIX = "PRI * HTTP/2"; /** * All stats for the HTTP/2 codec. @see stats_macros.h */ -#define ALL_HTTP2_CODEC_STATS(COUNTER) \ +#define ALL_HTTP2_CODEC_STATS(COUNTER, GAUGE) \ COUNTER(dropped_headers_with_underscores) \ COUNTER(header_overflow) \ COUNTER(headers_cb_no_stream) \ @@ -54,13 +54,16 @@ const std::string CLIENT_MAGIC_PREFIX = "PRI * HTTP/2"; COUNTER(rx_reset) \ COUNTER(too_many_header_frames) \ COUNTER(trailers) \ - COUNTER(tx_reset) + COUNTER(tx_flush_timeout) \ + COUNTER(tx_reset) \ + GAUGE(streams_active, Accumulate) \ + GAUGE(pending_send_bytes, Accumulate) /** * Wrapper struct for the HTTP/2 codec stats. @see stats_macros.h */ struct CodecStats { - ALL_HTTP2_CODEC_STATS(GENERATE_COUNTER_STRUCT) + ALL_HTTP2_CODEC_STATS(GENERATE_COUNTER_STRUCT, GENERATE_GAUGE_STRUCT) }; class Utility { @@ -150,6 +153,11 @@ class ConnectionImpl : public virtual Connection, protected Logger::Loggable; @@ -253,6 +269,10 @@ class ConnectionImpl : public virtual Connection, protected Logger::Loggable void { stream_callbacks_ = &callbacks; })); + EXPECT_CALL(stream_, setFlushTimeout(_)); EXPECT_CALL(stream_, bufferLimit()).WillOnce(Return(initial_buffer_limit_)); } diff --git a/test/common/http/http2/codec_impl_test.cc b/test/common/http/http2/codec_impl_test.cc index 9a83efd50218..cc35b9a0d96f 100644 --- a/test/common/http/http2/codec_impl_test.cc +++ b/test/common/http/http2/codec_impl_test.cc @@ -59,17 +59,36 @@ class Http2CodecImplTestFixture { }; Http2CodecImplTestFixture(Http2SettingsTuple client_settings, Http2SettingsTuple server_settings) - : client_settings_(client_settings), server_settings_(server_settings) {} - virtual ~Http2CodecImplTestFixture() = default; + : client_settings_(client_settings), server_settings_(server_settings) { + // Make sure we explicitly test for stream flush timer creation. + EXPECT_CALL(client_connection_.dispatcher_, createTimer_(_)).Times(0); + EXPECT_CALL(server_connection_.dispatcher_, createTimer_(_)).Times(0); + } + virtual ~Http2CodecImplTestFixture() { + client_connection_.dispatcher_.clearDeferredDeleteList(); + if (client_ != nullptr) { + client_.reset(); + EXPECT_EQ(0, TestUtility::findGauge(client_stats_store_, "http2.streams_active")->value()); + EXPECT_EQ(0, + TestUtility::findGauge(client_stats_store_, "http2.pending_send_bytes")->value()); + } + server_connection_.dispatcher_.clearDeferredDeleteList(); + if (server_ != nullptr) { + server_.reset(); + EXPECT_EQ(0, TestUtility::findGauge(server_stats_store_, "http2.streams_active")->value()); + EXPECT_EQ(0, + TestUtility::findGauge(server_stats_store_, "http2.pending_send_bytes")->value()); + } + } virtual void initialize() { Http2SettingsFromTuple(client_http2settings_, client_settings_); Http2SettingsFromTuple(server_http2settings_, server_settings_); client_ = std::make_unique( - client_connection_, client_callbacks_, stats_store_, client_http2settings_, + client_connection_, client_callbacks_, client_stats_store_, client_http2settings_, max_request_headers_kb_, max_response_headers_count_); server_ = std::make_unique( - server_connection_, server_callbacks_, stats_store_, server_http2settings_, + server_connection_, server_callbacks_, server_stats_store_, server_http2settings_, max_request_headers_kb_, max_request_headers_count_, headers_with_underscores_action_); request_encoder_ = &client_->newStream(response_decoder_); @@ -79,6 +98,7 @@ class Http2CodecImplTestFixture { .WillRepeatedly(Invoke([&](StreamEncoder& encoder, bool) -> StreamDecoder& { response_encoder_ = &encoder; encoder.getStream().addCallbacks(server_stream_callbacks_); + encoder.getStream().setFlushTimeout(std::chrono::milliseconds(30000)); return request_decoder_; })); } @@ -136,12 +156,13 @@ class Http2CodecImplTestFixture { const Http2SettingsTuple server_settings_; bool allow_metadata_ = false; bool stream_error_on_invalid_http_messaging_ = false; - Stats::IsolatedStoreImpl stats_store_; + Stats::IsolatedStoreImpl client_stats_store_; Http2Settings client_http2settings_; NiceMock client_connection_; MockConnectionCallbacks client_callbacks_; std::unique_ptr client_; ConnectionWrapper client_wrapper_; + Stats::IsolatedStoreImpl server_stats_store_; Http2Settings server_http2settings_; NiceMock server_connection_; MockServerConnectionCallbacks server_callbacks_; @@ -286,7 +307,7 @@ TEST_P(Http2CodecImplTest, InvalidContinueWithFin) { TestHeaderMapImpl continue_headers{{":status", "100"}}; EXPECT_THROW(response_encoder_->encodeHeaders(continue_headers, true), CodecProtocolException); - EXPECT_EQ(1, stats_store_.counter("http2.rx_messaging_error").value()); + EXPECT_EQ(1, client_stats_store_.counter("http2.rx_messaging_error").value()); } TEST_P(Http2CodecImplTest, InvalidContinueWithFinAllowed) { @@ -314,7 +335,7 @@ TEST_P(Http2CodecImplTest, InvalidContinueWithFinAllowed) { setupDefaultConnectionMocks(); client_wrapper_.dispatch(Buffer::OwnedImpl(), *client_); - EXPECT_EQ(1, stats_store_.counter("http2.rx_messaging_error").value()); + EXPECT_EQ(1, client_stats_store_.counter("http2.rx_messaging_error").value()); } TEST_P(Http2CodecImplTest, InvalidRepeatContinue) { @@ -330,7 +351,7 @@ TEST_P(Http2CodecImplTest, InvalidRepeatContinue) { response_encoder_->encode100ContinueHeaders(continue_headers); EXPECT_THROW(response_encoder_->encodeHeaders(continue_headers, true), CodecProtocolException); - EXPECT_EQ(1, stats_store_.counter("http2.rx_messaging_error").value()); + EXPECT_EQ(1, client_stats_store_.counter("http2.rx_messaging_error").value()); }; TEST_P(Http2CodecImplTest, InvalidRepeatContinueAllowed) { @@ -361,7 +382,7 @@ TEST_P(Http2CodecImplTest, InvalidRepeatContinueAllowed) { setupDefaultConnectionMocks(); client_wrapper_.dispatch(Buffer::OwnedImpl(), *client_); - EXPECT_EQ(1, stats_store_.counter("http2.rx_messaging_error").value()); + EXPECT_EQ(1, client_stats_store_.counter("http2.rx_messaging_error").value()); }; TEST_P(Http2CodecImplTest, Invalid103) { @@ -382,7 +403,7 @@ TEST_P(Http2CodecImplTest, Invalid103) { EXPECT_THROW_WITH_MESSAGE(response_encoder_->encodeHeaders(early_hint_headers, false), CodecProtocolException, "Unexpected 'trailers' with no end stream."); - EXPECT_EQ(1, stats_store_.counter("http2.too_many_header_frames").value()); + EXPECT_EQ(1, client_stats_store_.counter("http2.too_many_header_frames").value()); } TEST_P(Http2CodecImplTest, Invalid204WithContentLength) { @@ -403,7 +424,7 @@ TEST_P(Http2CodecImplTest, Invalid204WithContentLength) { } EXPECT_THROW(response_encoder_->encodeHeaders(response_headers, false), CodecProtocolException); - EXPECT_EQ(1, stats_store_.counter("http2.rx_messaging_error").value()); + EXPECT_EQ(1, client_stats_store_.counter("http2.rx_messaging_error").value()); }; TEST_P(Http2CodecImplTest, Invalid204WithContentLengthAllowed) { @@ -440,7 +461,7 @@ TEST_P(Http2CodecImplTest, Invalid204WithContentLengthAllowed) { setupDefaultConnectionMocks(); client_wrapper_.dispatch(Buffer::OwnedImpl(), *client_); - EXPECT_EQ(1, stats_store_.counter("http2.rx_messaging_error").value()); + EXPECT_EQ(1, client_stats_store_.counter("http2.rx_messaging_error").value()); }; TEST_P(Http2CodecImplTest, RefusedStreamReset) { @@ -463,7 +484,7 @@ TEST_P(Http2CodecImplTest, InvalidHeadersFrame) { initialize(); EXPECT_THROW(request_encoder_->encodeHeaders(TestHeaderMapImpl{}, true), CodecProtocolException); - EXPECT_EQ(1, stats_store_.counter("http2.rx_messaging_error").value()); + EXPECT_EQ(1, server_stats_store_.counter("http2.rx_messaging_error").value()); } TEST_P(Http2CodecImplTest, InvalidHeadersFrameAllowed) { @@ -525,7 +546,7 @@ TEST_P(Http2CodecImplTest, TrailingHeaders) { response_encoder_->encodeTrailers(TestHeaderMapImpl{{"trailing", "header"}}); } -TEST_P(Http2CodecImplTest, TrailingHeadersLargeBody) { +TEST_P(Http2CodecImplTest, TrailingHeadersLargeClientBody) { initialize(); // Buffer server data so we can make sure we don't get any window updates. @@ -540,11 +561,11 @@ TEST_P(Http2CodecImplTest, TrailingHeadersLargeBody) { EXPECT_CALL(request_decoder_, decodeData(_, false)).Times(AtLeast(1)); Buffer::OwnedImpl body(std::string(1024 * 1024, 'a')); request_encoder_->encodeData(body, false); - EXPECT_CALL(request_decoder_, decodeTrailers_(_)); request_encoder_->encodeTrailers(TestHeaderMapImpl{{"trailing", "header"}}); // Flush pending data. setupDefaultConnectionMocks(); + EXPECT_CALL(request_decoder_, decodeTrailers_(_)); server_wrapper_.dispatch(Buffer::OwnedImpl(), *server_); TestHeaderMapImpl response_headers{{":status", "200"}}; @@ -697,8 +718,11 @@ TEST_P(Http2CodecImplDeferredResetTest, DeferredResetServer) { response_encoder_->encodeHeaders(response_headers, false); Buffer::OwnedImpl body(std::string(1024 * 1024, 'a')); EXPECT_CALL(server_stream_callbacks_, onAboveWriteBufferHighWatermark()).Times(AnyNumber()); + auto flush_timer = new Event::MockTimer(&server_connection_.dispatcher_); + EXPECT_CALL(*flush_timer, enableTimer(std::chrono::milliseconds(30000), _)); response_encoder_->encodeData(body, true); EXPECT_CALL(server_stream_callbacks_, onResetStream(StreamResetReason::LocalReset, _)); + EXPECT_CALL(*flush_timer, disableTimer()); response_encoder_->getStream().resetStream(StreamResetReason::LocalReset); MockStreamCallbacks client_stream_callbacks; @@ -732,6 +756,8 @@ TEST_P(Http2CodecImplFlowControlTest, TestFlowControlInPendingSendData) { // Force the server stream to be read disabled. This will cause it to stop sending window // updates to the client. server_->getStream(1)->readDisable(true); + EXPECT_EQ(1, TestUtility::findGauge(client_stats_store_, "http2.streams_active")->value()); + EXPECT_EQ(1, TestUtility::findGauge(server_stats_store_, "http2.streams_active")->value()); uint32_t initial_stream_window = nghttp2_session_get_stream_effective_local_window_size(client_->session(), 1); @@ -757,6 +783,8 @@ TEST_P(Http2CodecImplFlowControlTest, TestFlowControlInPendingSendData) { Buffer::OwnedImpl more_long_data(std::string(initial_stream_window, 'a')); request_encoder_->encodeData(more_long_data, false); EXPECT_EQ(initial_stream_window, client_->getStream(1)->pending_send_data_.length()); + EXPECT_EQ(initial_stream_window, + TestUtility::findGauge(client_stats_store_, "http2.pending_send_bytes")->value()); EXPECT_EQ(initial_stream_window, server_->getStream(1)->unconsumed_bytes_); // If we go over the limit, the stream callbacks should fire. @@ -764,6 +792,8 @@ TEST_P(Http2CodecImplFlowControlTest, TestFlowControlInPendingSendData) { Buffer::OwnedImpl last_byte("!"); request_encoder_->encodeData(last_byte, false); EXPECT_EQ(initial_stream_window + 1, client_->getStream(1)->pending_send_data_.length()); + EXPECT_EQ(initial_stream_window + 1, + TestUtility::findGauge(client_stats_store_, "http2.pending_send_bytes")->value()); // Now create a second stream on the connection. MockStreamDecoder response_decoder2; @@ -807,6 +837,7 @@ TEST_P(Http2CodecImplFlowControlTest, TestFlowControlInPendingSendData) { EXPECT_CALL(callbacks3, onBelowWriteBufferLowWatermark()); server_->getStream(1)->readDisable(false); EXPECT_EQ(0, client_->getStream(1)->pending_send_data_.length()); + EXPECT_EQ(0, TestUtility::findGauge(client_stats_store_, "http2.pending_send_bytes")->value()); // The extra 1 byte sent won't trigger another window update, so the final window should be the // initial window minus the last 1 byte flush from the client to server. EXPECT_EQ(initial_stream_window - 1, @@ -899,6 +930,108 @@ TEST_P(Http2CodecImplFlowControlTest, FlowControlPendingRecvData) { request_encoder_->encodeData(data, false); } +// Verify that we create and disable the stream flush timer when trailers follow a stream that +// does not have enough window. +TEST_P(Http2CodecImplFlowControlTest, TrailingHeadersLargeServerBody) { + initialize(); + + InSequence s; + TestHeaderMapImpl request_headers; + HttpTestUtility::addDefaultHeaders(request_headers); + EXPECT_CALL(request_decoder_, decodeHeaders_(_, true)); + request_encoder_->encodeHeaders(request_headers, true); + + ON_CALL(client_connection_, write(_, _)) + .WillByDefault( + Invoke([&](Buffer::Instance& data, bool) -> void { server_wrapper_.buffer_.add(data); })); + TestHeaderMapImpl response_headers{{":status", "200"}}; + EXPECT_CALL(response_decoder_, decodeHeaders_(_, false)); + response_encoder_->encodeHeaders(response_headers, false); + EXPECT_CALL(server_stream_callbacks_, onAboveWriteBufferHighWatermark()); + EXPECT_CALL(response_decoder_, decodeData(_, false)).Times(AtLeast(1)); + auto flush_timer = new Event::MockTimer(&server_connection_.dispatcher_); + EXPECT_CALL(*flush_timer, enableTimer(std::chrono::milliseconds(30000), _)); + Buffer::OwnedImpl body(std::string(1024 * 1024, 'a')); + response_encoder_->encodeData(body, false); + response_encoder_->encodeTrailers(TestHeaderMapImpl{{"trailing", "header"}}); + + // Send window updates from the client. + setupDefaultConnectionMocks(); + EXPECT_CALL(response_decoder_, decodeData(_, false)).Times(AtLeast(1)); + EXPECT_CALL(response_decoder_, decodeTrailers_(_)); + EXPECT_CALL(*flush_timer, disableTimer()); + server_wrapper_.dispatch(Buffer::OwnedImpl(), *server_); + EXPECT_EQ(0, server_stats_store_.counter("http2.tx_flush_timeout").value()); +} + +// Verify that we create and handle the stream flush timeout when trailers follow a stream that +// does not have enough window. +TEST_P(Http2CodecImplFlowControlTest, TrailingHeadersLargeServerBodyFlushTimeout) { + initialize(); + + InSequence s; + MockStreamCallbacks client_stream_callbacks; + request_encoder_->getStream().addCallbacks(client_stream_callbacks); + TestHeaderMapImpl request_headers; + HttpTestUtility::addDefaultHeaders(request_headers); + EXPECT_CALL(request_decoder_, decodeHeaders_(_, true)); + request_encoder_->encodeHeaders(request_headers, true); + + ON_CALL(client_connection_, write(_, _)) + .WillByDefault( + Invoke([&](Buffer::Instance& data, bool) -> void { server_wrapper_.buffer_.add(data); })); + TestHeaderMapImpl response_headers{{":status", "200"}}; + EXPECT_CALL(response_decoder_, decodeHeaders_(_, false)); + response_encoder_->encodeHeaders(response_headers, false); + EXPECT_CALL(server_stream_callbacks_, onAboveWriteBufferHighWatermark()); + EXPECT_CALL(response_decoder_, decodeData(_, false)).Times(AtLeast(1)); + auto flush_timer = new Event::MockTimer(&server_connection_.dispatcher_); + EXPECT_CALL(*flush_timer, enableTimer(std::chrono::milliseconds(30000), _)); + Buffer::OwnedImpl body(std::string(1024 * 1024, 'a')); + response_encoder_->encodeData(body, false); + response_encoder_->encodeTrailers(TestHeaderMapImpl{{"trailing", "header"}}); + + // Invoke a stream flush timeout. Make sure we don't get a reset locally for higher layers but + // we do get a reset on the client. + EXPECT_CALL(server_stream_callbacks_, onResetStream(_, _)).Times(0); + EXPECT_CALL(client_stream_callbacks, onResetStream(StreamResetReason::RemoteReset, _)); + flush_timer->invokeCallback(); + EXPECT_EQ(1, server_stats_store_.counter("http2.tx_flush_timeout").value()); +} + +// Verify that we create and handle the stream flush timeout when there is a large body that +// does not have enough window. +TEST_P(Http2CodecImplFlowControlTest, LargeServerBodyFlushTimeout) { + initialize(); + + InSequence s; + MockStreamCallbacks client_stream_callbacks; + request_encoder_->getStream().addCallbacks(client_stream_callbacks); + TestHeaderMapImpl request_headers; + HttpTestUtility::addDefaultHeaders(request_headers); + EXPECT_CALL(request_decoder_, decodeHeaders_(_, true)); + request_encoder_->encodeHeaders(request_headers, true); + + ON_CALL(client_connection_, write(_, _)) + .WillByDefault( + Invoke([&](Buffer::Instance& data, bool) -> void { server_wrapper_.buffer_.add(data); })); + TestHeaderMapImpl response_headers{{":status", "200"}}; + EXPECT_CALL(response_decoder_, decodeHeaders_(_, false)); + response_encoder_->encodeHeaders(response_headers, false); + EXPECT_CALL(response_decoder_, decodeData(_, false)).Times(AtLeast(1)); + auto flush_timer = new Event::MockTimer(&server_connection_.dispatcher_); + EXPECT_CALL(*flush_timer, enableTimer(std::chrono::milliseconds(30000), _)); + Buffer::OwnedImpl body(std::string(1024 * 1024, 'a')); + response_encoder_->encodeData(body, true); + + // Invoke a stream flush timeout. Make sure we don't get a reset locally for higher layers but + // we do get a reset on the client. + EXPECT_CALL(server_stream_callbacks_, onResetStream(_, _)).Times(0); + EXPECT_CALL(client_stream_callbacks, onResetStream(StreamResetReason::RemoteReset, _)); + flush_timer->invokeCallback(); + EXPECT_EQ(1, server_stats_store_.counter("http2.tx_flush_timeout").value()); +} + TEST_P(Http2CodecImplTest, WatermarkUnderEndStream) { initialize(); MockStreamCallbacks callbacks; @@ -953,10 +1086,10 @@ TEST_P(Http2CodecImplStreamLimitTest, MaxClientStreams) { Http2SettingsFromTuple(client_http2settings_, ::testing::get<0>(GetParam())); Http2SettingsFromTuple(server_http2settings_, ::testing::get<1>(GetParam())); client_ = std::make_unique( - client_connection_, client_callbacks_, stats_store_, client_http2settings_, + client_connection_, client_callbacks_, client_stats_store_, client_http2settings_, max_request_headers_kb_, max_response_headers_count_); server_ = std::make_unique( - server_connection_, server_callbacks_, stats_store_, server_http2settings_, + server_connection_, server_callbacks_, server_stats_store_, server_http2settings_, max_request_headers_kb_, max_request_headers_count_, headers_with_underscores_action_); for (int i = 0; i < 101; ++i) { @@ -1104,7 +1237,7 @@ TEST_P(Http2CodecImplTest, HeaderNameWithUnderscoreAreDropped) { request_headers.addCopy("bad_header", "something"); EXPECT_CALL(request_decoder_, decodeHeaders_(HeaderMapEqual(&expected_headers), _)); request_encoder_->encodeHeaders(request_headers, false); - EXPECT_EQ(1, stats_store_.counter("http2.dropped_headers_with_underscores").value()); + EXPECT_EQ(1, server_stats_store_.counter("http2.dropped_headers_with_underscores").value()); } // Tests that request with header names containing underscore are rejected when the option is set to @@ -1118,7 +1251,9 @@ TEST_P(Http2CodecImplTest, HeaderNameWithUnderscoreAreRejectedByDefault) { request_headers.addCopy("bad_header", "something"); EXPECT_CALL(server_stream_callbacks_, onResetStream(_, _)).Times(1); request_encoder_->encodeHeaders(request_headers, false); - EXPECT_EQ(1, stats_store_.counter("http2.requests_rejected_with_underscores_in_headers").value()); + EXPECT_EQ( + 1, + server_stats_store_.counter("http2.requests_rejected_with_underscores_in_headers").value()); } // Tests request headers with name containing underscore are allowed when the option is set to @@ -1134,7 +1269,7 @@ TEST_P(Http2CodecImplTest, HeaderNameWithUnderscoreAllowed) { EXPECT_CALL(request_decoder_, decodeHeaders_(HeaderMapEqual(&expected_headers), _)); EXPECT_CALL(server_stream_callbacks_, onResetStream(_, _)).Times(0); request_encoder_->encodeHeaders(request_headers, false); - EXPECT_EQ(0, stats_store_.counter("http2.dropped_headers_with_underscores").value()); + EXPECT_EQ(0, server_stats_store_.counter("http2.dropped_headers_with_underscores").value()); } // This is the HTTP/2 variant of the HTTP/1 regression test for CVE-2019-18801. @@ -1349,7 +1484,7 @@ TEST_P(Http2CodecImplTest, PingFlood) { EXPECT_THROW(client_->sendPendingFrames(), FrameFloodException); EXPECT_EQ(ack_count, Http2Settings::DEFAULT_MAX_OUTBOUND_CONTROL_FRAMES); - EXPECT_EQ(1, stats_store_.counter("http2.outbound_control_flood").value()); + EXPECT_EQ(1, server_stats_store_.counter("http2.outbound_control_flood").value()); } // Verify that codec allows PING flood when mitigation is disabled @@ -1447,7 +1582,7 @@ TEST_P(Http2CodecImplTest, ResponseHeadersFlood) { EXPECT_THROW(client_->sendPendingFrames(), FrameFloodException); EXPECT_EQ(frame_count, Http2Settings::DEFAULT_MAX_OUTBOUND_FRAMES + 1); - EXPECT_EQ(1, stats_store_.counter("http2.outbound_flood").value()); + EXPECT_EQ(1, server_stats_store_.counter("http2.outbound_flood").value()); } // Verify that codec detects flood of outbound DATA frames @@ -1480,7 +1615,7 @@ TEST_P(Http2CodecImplTest, ResponseDataFlood) { EXPECT_THROW(client_->sendPendingFrames(), FrameFloodException); EXPECT_EQ(frame_count, Http2Settings::DEFAULT_MAX_OUTBOUND_FRAMES + 1); - EXPECT_EQ(1, stats_store_.counter("http2.outbound_flood").value()); + EXPECT_EQ(1, server_stats_store_.counter("http2.outbound_flood").value()); } // Verify that codec allows outbound DATA flood when mitigation is disabled @@ -1584,7 +1719,7 @@ TEST_P(Http2CodecImplTest, PingStacksWithDataFlood) { EXPECT_THROW(client_->sendPendingFrames(), FrameFloodException); EXPECT_EQ(frame_count, Http2Settings::DEFAULT_MAX_OUTBOUND_FRAMES); - EXPECT_EQ(1, stats_store_.counter("http2.outbound_flood").value()); + EXPECT_EQ(1, server_stats_store_.counter("http2.outbound_flood").value()); } TEST_P(Http2CodecImplTest, PriorityFlood) { diff --git a/test/extensions/transport_sockets/alts/alts_integration_test.cc b/test/extensions/transport_sockets/alts/alts_integration_test.cc index 23748aa098b8..90f2c49cff45 100644 --- a/test/extensions/transport_sockets/alts/alts_integration_test.cc +++ b/test/extensions/transport_sockets/alts/alts_integration_test.cc @@ -204,7 +204,7 @@ INSTANTIATE_TEST_SUITE_P(IpVersions, AltsIntegrationTestClientInvalidPeer, // any account in config, the handshake will fail and client closes connection. TEST_P(AltsIntegrationTestClientInvalidPeer, clientValidationFail) { initialize(); - codec_client_ = makeRawHttpConnection(makeAltsConnection()); + codec_client_ = makeRawHttpConnection(makeAltsConnection(), absl::nullopt); EXPECT_FALSE(codec_client_->connected()); } @@ -252,7 +252,7 @@ INSTANTIATE_TEST_SUITE_P(IpVersions, AltsIntegrationTestClientWrongHandshaker, // and connection closes. TEST_P(AltsIntegrationTestClientWrongHandshaker, ConnectToWrongHandshakerAddress) { initialize(); - codec_client_ = makeRawHttpConnection(makeAltsConnection()); + codec_client_ = makeRawHttpConnection(makeAltsConnection(), absl::nullopt); EXPECT_FALSE(codec_client_->connected()); } diff --git a/test/extensions/transport_sockets/tls/integration/ssl_integration_test.cc b/test/extensions/transport_sockets/tls/integration/ssl_integration_test.cc index c3d0960dc06c..a353b1aa508f 100644 --- a/test/extensions/transport_sockets/tls/integration/ssl_integration_test.cc +++ b/test/extensions/transport_sockets/tls/integration/ssl_integration_test.cc @@ -276,7 +276,8 @@ TEST_P(SslCertficateIntegrationTest, ServerEcdsaClientRsaOnly) { server_rsa_cert_ = false; server_ecdsa_cert_ = true; initialize(); - auto codec_client = makeRawHttpConnection(makeSslClientConnection(rsaOnlyClientOptions())); + auto codec_client = + makeRawHttpConnection(makeSslClientConnection(rsaOnlyClientOptions()), absl::nullopt); EXPECT_FALSE(codec_client->connected()); const std::string counter_name = listenerStatPrefix("ssl.connection_error"); Stats::CounterSharedPtr counter = test_server_->counter(counter_name); @@ -303,7 +304,8 @@ TEST_P(SslCertficateIntegrationTest, ServerRsaClientEcdsaOnly) { client_ecdsa_cert_ = true; initialize(); EXPECT_FALSE( - makeRawHttpConnection(makeSslClientConnection(ecdsaOnlyClientOptions()))->connected()); + makeRawHttpConnection(makeSslClientConnection(ecdsaOnlyClientOptions()), absl::nullopt) + ->connected()); const std::string counter_name = listenerStatPrefix("ssl.connection_error"); Stats::CounterSharedPtr counter = test_server_->counter(counter_name); test_server_->waitForCounterGe(counter_name, 1); diff --git a/test/integration/http2_integration_test.cc b/test/integration/http2_integration_test.cc index 80b03bd88ad2..830f043374e0 100644 --- a/test/integration/http2_integration_test.cc +++ b/test/integration/http2_integration_test.cc @@ -66,6 +66,28 @@ TEST_P(Http2IntegrationTest, RetryAttemptCount) { testRetryAttemptCountHeader(); TEST_P(Http2IntegrationTest, LargeRequestTrailersRejected) { testLargeRequestTrailers(66, 60); } +// Verify downstream codec stream flush timeout. +TEST_P(Http2IntegrationTest, CodecStreamIdleTimeout) { + config_helper_.setBufferLimits(1024, 1024); + config_helper_.addConfigModifier( + [&](envoy::config::filter::network::http_connection_manager::v2::HttpConnectionManager& hcm) + -> void { + hcm.mutable_stream_idle_timeout()->set_seconds(0); + constexpr uint64_t IdleTimeoutMs = 400; + hcm.mutable_stream_idle_timeout()->set_nanos(IdleTimeoutMs * 1000 * 1000); + }); + initialize(); + Http::Http2Settings http2_options; + http2_options.initial_stream_window_size_ = 65535; + codec_client_ = makeRawHttpConnection(makeClientConnection(lookupPort("http")), http2_options); + auto response = codec_client_->makeHeaderOnlyRequest(default_request_headers_); + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders(default_response_headers_, false); + upstream_request_->encodeData(70000, true); + test_server_->waitForCounterEq("http2.tx_flush_timeout", 1); + response->waitForReset(); +} + static std::string response_metadata_filter = R"EOF( name: response-metadata-filter typed_config: diff --git a/test/integration/http2_upstream_integration_test.cc b/test/integration/http2_upstream_integration_test.cc index 8b32a232f97d..15f00afddcc1 100644 --- a/test/integration/http2_upstream_integration_test.cc +++ b/test/integration/http2_upstream_integration_test.cc @@ -237,6 +237,9 @@ void Http2UpstreamIntegrationTest::manySimultaneousRequests(uint32_t request_byt EXPECT_EQ("503", responses[i]->headers().Status()->value().getStringView()); } } + + EXPECT_EQ(0, test_server_->gauge("http2.streams_active")->value()); + EXPECT_EQ(0, test_server_->gauge("http2.pending_send_bytes")->value()); } TEST_P(Http2UpstreamIntegrationTest, ManySimultaneousRequest) { diff --git a/test/integration/http_integration.cc b/test/integration/http_integration.cc index 6a8a52004b19..681a363e9a73 100644 --- a/test/integration/http_integration.cc +++ b/test/integration/http_integration.cc @@ -189,11 +189,16 @@ IntegrationCodecClientPtr HttpIntegrationTest::makeHttpConnection(uint32_t port) } IntegrationCodecClientPtr -HttpIntegrationTest::makeRawHttpConnection(Network::ClientConnectionPtr&& conn) { +HttpIntegrationTest::makeRawHttpConnection(Network::ClientConnectionPtr&& conn, + absl::optional http2_options) { std::shared_ptr cluster{new NiceMock()}; cluster->max_response_headers_count_ = 200; - cluster->http2_settings_.allow_connect_ = true; - cluster->http2_settings_.allow_metadata_ = true; + if (http2_options.has_value()) { + cluster->http2_settings_ = http2_options.value(); + } else { + cluster->http2_settings_.allow_connect_ = true; + cluster->http2_settings_.allow_metadata_ = true; + } Upstream::HostDescriptionConstSharedPtr host_description{Upstream::makeTestHostDescription( cluster, fmt::format("tcp://{}:80", Network::Test::getLoopbackAddressUrlString(version_)))}; return std::make_unique(*dispatcher_, std::move(conn), host_description, @@ -202,7 +207,7 @@ HttpIntegrationTest::makeRawHttpConnection(Network::ClientConnectionPtr&& conn) IntegrationCodecClientPtr HttpIntegrationTest::makeHttpConnection(Network::ClientConnectionPtr&& conn) { - auto codec = makeRawHttpConnection(std::move(conn)); + auto codec = makeRawHttpConnection(std::move(conn), absl::nullopt); EXPECT_TRUE(codec->connected()); return codec; } diff --git a/test/integration/http_integration.h b/test/integration/http_integration.h index 7882bd3e85d5..9839fd55ae9b 100644 --- a/test/integration/http_integration.h +++ b/test/integration/http_integration.h @@ -108,7 +108,9 @@ class HttpIntegrationTest : public BaseIntegrationTest { IntegrationCodecClientPtr makeHttpConnection(uint32_t port); // Makes a http connection object without checking its connected state. - IntegrationCodecClientPtr makeRawHttpConnection(Network::ClientConnectionPtr&& conn); + virtual IntegrationCodecClientPtr + makeRawHttpConnection(Network::ClientConnectionPtr&& conn, + absl::optional http2_options); // Makes a http connection object with asserting a connected state. IntegrationCodecClientPtr makeHttpConnection(Network::ClientConnectionPtr&& conn); diff --git a/test/integration/integration_admin_test.cc b/test/integration/integration_admin_test.cc index 7e39838bc1a6..548181de30e2 100644 --- a/test/integration/integration_admin_test.cc +++ b/test/integration/integration_admin_test.cc @@ -278,14 +278,17 @@ TEST_P(IntegrationAdminTest, Admin) { " 1 http2.inbound_window_update_frames_flood\n" " 1 http2.outbound_control_flood\n" " 1 http2.outbound_flood\n" + " 1 http2.pending_send_bytes\n" " 1 http2.requests_rejected_with_underscores_in_headers\n" " 1 http2.rx_messaging_error\n" " 1 http2.rx_reset\n" + " 1 http2.streams_active\n" " 1 http2.too_many_header_frames\n" " 1 http2.trailers\n" + " 1 http2.tx_flush_timeout\n" " 1 http2.tx_reset\n" "\n" - "total: 14\n", + "total: 17\n", response->body()); break; case Http::CodecClient::Type::HTTP3: diff --git a/test/integration/sds_dynamic_integration_test.cc b/test/integration/sds_dynamic_integration_test.cc index 38d8391444e9..e86a28a79e61 100644 --- a/test/integration/sds_dynamic_integration_test.cc +++ b/test/integration/sds_dynamic_integration_test.cc @@ -222,7 +222,7 @@ TEST_P(SdsDynamicDownstreamIntegrationTest, WrongSecretFirst) { }; initialize(); - codec_client_ = makeRawHttpConnection(makeSslClientConnection()); + codec_client_ = makeRawHttpConnection(makeSslClientConnection(), absl::nullopt); // the connection state is not connected. EXPECT_FALSE(codec_client_->connected()); codec_client_->connection()->close(Network::ConnectionCloseType::NoFlush); diff --git a/test/mocks/http/stream.h b/test/mocks/http/stream.h index 54b81c4fd912..16531b3c7977 100644 --- a/test/mocks/http/stream.h +++ b/test/mocks/http/stream.h @@ -19,6 +19,7 @@ class MockStream : public Stream { MOCK_METHOD1(readDisable, void(bool disable)); MOCK_METHOD2(setWriteBufferWatermarks, void(uint32_t, uint32_t)); MOCK_METHOD0(bufferLimit, uint32_t()); + MOCK_METHOD1(setFlushTimeout, void(std::chrono::milliseconds timeout)); std::list callbacks_{};