Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/root/configuration/http/http_filters/router_filter.rst
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,19 @@ or :ref:`regex_rewrite <envoy_v3_api_field_config.route.v3.RouteAction.regex_rew
Envoy will put the original path header in this header. This can be useful for logging and
debugging.

.. _config_http_filters_router_x-envoy-upstream-stream-duration-ms:

x-envoy-upstream-stream-duration-ms
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

This value is used to configure the maximum upstream stream lifetime for the stream which has this header.
If the stream exceeds this lifetime, it will be reset and a 408 response
will be sent to downstream. If the value of the header is 0, then the lifetime will be
infinite and no limit will be enforced. It is similar to
:ref:`max_stream_duration <envoy_v3_api_field_config.core.v3.HttpProtocolOptions.max_stream_duration>`,
but that configuration applies to all streams to this cluster. If set, this header will
override the cluster configuration. The value set for this header is set independently for other timeout related headers.

HTTP response headers set on downstream responses
-------------------------------------------------

Expand Down
1 change: 1 addition & 0 deletions docs/root/version_history/current.rst
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ New Features
* contrib: added new :ref:`contrib images <install_contrib>` which contain contrib extensions.
* grpc reverse bridge: added a new :ref:`option <envoy_v3_api_field_extensions.filters.http.grpc_http1_reverse_bridge.v3.FilterConfig.response_size_header>` to support streaming response bodies when withholding gRPC frames from the upstream.
* http: added :ref:`string_match <envoy_v3_api_field_config.route.v3.HeaderMatcher.string_match>` in the header matcher.
* http: added :ref:`x-envoy-upstream-stream-duration-ms <config_http_filters_router_x-envoy-upstream-stream-duration-ms>` that allows configuring the max stream duration via a request header.
* http: added support for :ref:`max_requests_per_connection <envoy_v3_api_field_config.core.v3.HttpProtocolOptions.max_requests_per_connection>` for both upstream and downstream connections.
* http: sanitizing the referer header as documented :ref:`here <config_http_conn_man_headers_referer>`. This feature can be temporarily turned off by setting runtime guard ``envoy.reloadable_features.sanitize_http_header_referer`` to false.
* jwt_authn: added support for :ref:`Jwt Cache <envoy_v3_api_field_extensions.filters.http.jwt_authn.v3.JwtProvider.jwt_cache_config>` and its size can be specified by :ref:`jwt_cache_size <envoy_v3_api_field_extensions.filters.http.jwt_authn.v3.JwtCacheConfig.jwt_cache_size>`.
Expand Down
3 changes: 2 additions & 1 deletion envoy/http/header_map.h
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,8 @@ class HeaderEntry {
HEADER_FUNC(EnvoyExpectedRequestTimeoutMs) \
HEADER_FUNC(EnvoyMaxRetries) \
HEADER_FUNC(EnvoyUpstreamRequestTimeoutMs) \
HEADER_FUNC(EnvoyUpstreamRequestPerTryTimeoutMs)
HEADER_FUNC(EnvoyUpstreamRequestPerTryTimeoutMs) \
HEADER_FUNC(EnvoyUpstreamStreamDurationMs)

#define INLINE_REQ_HEADERS(HEADER_FUNC) \
INLINE_REQ_STRING_HEADERS(HEADER_FUNC) \
Expand Down
1 change: 0 additions & 1 deletion envoy/router/router.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
#include "envoy/http/codes.h"
#include "envoy/http/conn_pool.h"
#include "envoy/http/hash_policy.h"
#include "envoy/http/header_map.h"
#include "envoy/router/internal_redirect.h"
#include "envoy/tcp/conn_pool.h"
#include "envoy/tracing/http_tracer.h"
Expand Down
2 changes: 2 additions & 0 deletions source/common/http/headers.h
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ class HeaderValues {
const LowerCaseString EnvoyUpstreamServiceTime{absl::StrCat(prefix(), "-upstream-service-time")};
const LowerCaseString EnvoyUpstreamHealthCheckedCluster{
absl::StrCat(prefix(), "-upstream-healthchecked-cluster")};
const LowerCaseString EnvoyUpstreamStreamDurationMs{
absl::StrCat(prefix(), "-upstream-stream-duration-ms")};
const LowerCaseString EnvoyDecoratorOperation{absl::StrCat(prefix(), "-decorator-operation")};
const LowerCaseString Expect{"expect"};
const LowerCaseString ForwardedClientCert{"x-forwarded-client-cert"};
Expand Down
38 changes: 27 additions & 11 deletions source/common/router/router.cc
Original file line number Diff line number Diff line change
Expand Up @@ -171,18 +171,21 @@ FilterUtility::finalTimeout(const RouteEntry& route, Http::RequestHeaderMap& req
const Http::HeaderEntry* header_expected_timeout_entry =
request_headers.EnvoyExpectedRequestTimeoutMs();
if (header_expected_timeout_entry) {
trySetGlobalTimeout(header_expected_timeout_entry, timeout);
trySetGlobalTimeout(*header_expected_timeout_entry, timeout);
} else {
const Http::HeaderEntry* header_timeout_entry =
request_headers.EnvoyUpstreamRequestTimeoutMs();

if (trySetGlobalTimeout(header_timeout_entry, timeout)) {
if (header_timeout_entry) {
trySetGlobalTimeout(*header_timeout_entry, timeout);
request_headers.removeEnvoyUpstreamRequestTimeoutMs();
}
}
} else {
const Http::HeaderEntry* header_timeout_entry = request_headers.EnvoyUpstreamRequestTimeoutMs();
if (trySetGlobalTimeout(header_timeout_entry, timeout)) {

if (header_timeout_entry) {
trySetGlobalTimeout(*header_timeout_entry, timeout);
request_headers.removeEnvoyUpstreamRequestTimeoutMs();
}
}
Expand Down Expand Up @@ -226,16 +229,21 @@ FilterUtility::finalTimeout(const RouteEntry& route, Http::RequestHeaderMap& req
return timeout;
}

bool FilterUtility::trySetGlobalTimeout(const Http::HeaderEntry* header_timeout_entry,
absl::optional<std::chrono::milliseconds>
FilterUtility::tryParseHeaderTimeout(const Http::HeaderEntry& header_timeout_entry) {
uint64_t header_timeout;
if (absl::SimpleAtoi(header_timeout_entry.value().getStringView(), &header_timeout)) {
return std::chrono::milliseconds(header_timeout);
}
return absl::nullopt;
}

void FilterUtility::trySetGlobalTimeout(const Http::HeaderEntry& header_timeout_entry,
TimeoutData& timeout) {
if (header_timeout_entry) {
uint64_t header_timeout;
if (absl::SimpleAtoi(header_timeout_entry->value().getStringView(), &header_timeout)) {
timeout.global_timeout_ = std::chrono::milliseconds(header_timeout);
}
return true;
const auto timeout_ms = tryParseHeaderTimeout(header_timeout_entry);
if (timeout_ms.has_value()) {
timeout.global_timeout_ = timeout_ms.value();
}
return false;
}

FilterUtility::HedgingParams
Expand Down Expand Up @@ -579,6 +587,14 @@ Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers,
grpc_request_, hedging_params_.hedge_on_per_try_timeout_,
config_.respect_expected_rq_timeout_);

const Http::HeaderEntry* header_max_stream_duration_entry =
headers.EnvoyUpstreamStreamDurationMs();
if (header_max_stream_duration_entry) {
dynamic_max_stream_duration_ =
FilterUtility::tryParseHeaderTimeout(*header_max_stream_duration_entry);
headers.removeEnvoyUpstreamStreamDurationMs();
}

// If this header is set with any value, use an alternate response code on timeout
if (headers.EnvoyUpstreamRequestTimeoutAltResponse()) {
timeout_response_code_ = Http::Code::NoContent;
Expand Down
24 changes: 22 additions & 2 deletions source/common/router/router.h
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,22 @@ class FilterUtility {
bool per_try_timeout_hedging_enabled,
bool respect_expected_rq_timeout);

static bool trySetGlobalTimeout(const Http::HeaderEntry* header_timeout_entry,
/**
* Try to parse a header entry that may have a timeout field
*
* @param header_timeout_entry header entry which may contain a timeout value.
* @return result timeout value from header. It will return nullopt if parse failed.
*/
static absl::optional<std::chrono::milliseconds>
tryParseHeaderTimeout(const Http::HeaderEntry& header_timeout_entry);

/**
* Try to set global timeout.
*
* @param header_timeout_entry header entry which may contain a timeout value.
* @param timeout timeout data to set from header timeout entry.
*/
static void trySetGlobalTimeout(const Http::HeaderEntry& header_timeout_entry,
TimeoutData& timeout);

/**
Expand Down Expand Up @@ -262,6 +277,7 @@ class RouterFilterInterface {
virtual Upstream::ClusterInfoConstSharedPtr cluster() PURE;
virtual FilterConfig& config() PURE;
virtual FilterUtility::TimeoutData timeout() PURE;
virtual absl::optional<std::chrono::milliseconds> dynamicMaxStreamDuration() const PURE;
virtual Http::RequestHeaderMap* downstreamHeaders() PURE;
virtual Http::RequestTrailerMap* downstreamTrailers() PURE;
virtual bool downstreamResponseStarted() const PURE;
Expand Down Expand Up @@ -436,6 +452,9 @@ class Filter : Logger::Loggable<Logger::Id::router>,
Upstream::ClusterInfoConstSharedPtr cluster() override { return cluster_; }
FilterConfig& config() override { return config_; }
FilterUtility::TimeoutData timeout() override { return timeout_; }
absl::optional<std::chrono::milliseconds> dynamicMaxStreamDuration() const override {
return dynamic_max_stream_duration_;
}
Http::RequestHeaderMap* downstreamHeaders() override { return downstream_headers_; }
Http::RequestTrailerMap* downstreamTrailers() override { return downstream_trailers_; }
bool downstreamResponseStarted() const override { return downstream_response_started_; }
Expand Down Expand Up @@ -535,7 +554,8 @@ class Filter : Logger::Loggable<Logger::Id::router>,
MetadataMatchCriteriaConstPtr metadata_match_;
std::function<void(Http::ResponseHeaderMap&)> modify_headers_;
std::vector<std::reference_wrapper<const ShadowPolicy>> active_shadow_policies_{};

// The stream lifetime configured by request header.
absl::optional<std::chrono::milliseconds> dynamic_max_stream_duration_;
// list of cookies to add to upstream headers
std::vector<std::string> downstream_set_cookies_;

Expand Down
18 changes: 11 additions & 7 deletions source/common/router/upstream_request.cc
Original file line number Diff line number Diff line change
Expand Up @@ -457,14 +457,18 @@ void UpstreamRequest::onPoolReady(
paused_for_connect_ = true;
}

if (upstream_host_->cluster().commonHttpProtocolOptions().has_max_stream_duration()) {
const auto max_stream_duration = std::chrono::milliseconds(DurationUtil::durationToMilliseconds(
absl::optional<std::chrono::milliseconds> max_stream_duration;
if (parent_.dynamicMaxStreamDuration().has_value()) {
max_stream_duration = parent_.dynamicMaxStreamDuration().value();
} else if (upstream_host_->cluster().commonHttpProtocolOptions().has_max_stream_duration()) {
max_stream_duration = std::chrono::milliseconds(DurationUtil::durationToMilliseconds(
upstream_host_->cluster().commonHttpProtocolOptions().max_stream_duration()));
if (max_stream_duration.count()) {
max_stream_duration_timer_ = parent_.callbacks()->dispatcher().createTimer(
[this]() -> void { onStreamMaxDurationReached(); });
max_stream_duration_timer_->enableTimer(max_stream_duration);
}
}

if (max_stream_duration.has_value() && max_stream_duration->count()) {
max_stream_duration_timer_ = parent_.callbacks()->dispatcher().createTimer(
[this]() -> void { onStreamMaxDurationReached(); });
max_stream_duration_timer_->enableTimer(*max_stream_duration);
}

const Http::Status status =
Expand Down
52 changes: 47 additions & 5 deletions test/common/router/router_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3746,7 +3746,7 @@ TEST_F(RouterTest, MaxStreamDurationValidlyConfiguredWithoutRetryPolicy) {
upstream_stream_info_, Http::Protocol::Http10);
return nullptr;
}));
expectMaxStreamDurationTimerCreate();
expectMaxStreamDurationTimerCreate(std::chrono::milliseconds(500));

Http::TestRequestHeaderMapImpl headers;
HttpTestUtility::addDefaultHeaders(headers);
Expand Down Expand Up @@ -3795,7 +3795,7 @@ TEST_F(RouterTest, MaxStreamDurationCallbackNotCalled) {
upstream_stream_info_, Http::Protocol::Http10);
return nullptr;
}));
expectMaxStreamDurationTimerCreate();
expectMaxStreamDurationTimerCreate(std::chrono::milliseconds(5000));

Http::TestRequestHeaderMapImpl headers;
HttpTestUtility::addDefaultHeaders(headers);
Expand All @@ -3818,7 +3818,7 @@ TEST_F(RouterTest, MaxStreamDurationWhenDownstreamAlreadyStartedWithoutRetryPoli
upstream_stream_info_, Http::Protocol::Http10);
return nullptr;
}));
expectMaxStreamDurationTimerCreate();
expectMaxStreamDurationTimerCreate(std::chrono::milliseconds(500));

Http::TestRequestHeaderMapImpl headers;
HttpTestUtility::addDefaultHeaders(headers);
Expand Down Expand Up @@ -3846,7 +3846,7 @@ TEST_F(RouterTest, MaxStreamDurationWithRetryPolicy) {
upstream_stream_info_, Http::Protocol::Http10);
return nullptr;
}));
expectMaxStreamDurationTimerCreate();
expectMaxStreamDurationTimerCreate(std::chrono::milliseconds(500));

Http::TestRequestHeaderMapImpl headers{{"x-envoy-retry-on", "reset"},
{"x-envoy-internal", "true"}};
Expand All @@ -3868,7 +3868,7 @@ TEST_F(RouterTest, MaxStreamDurationWithRetryPolicy) {
upstream_stream_info_, Http::Protocol::Http10);
return nullptr;
}));
expectMaxStreamDurationTimerCreate();
expectMaxStreamDurationTimerCreate(std::chrono::milliseconds(500));
router_.retry_state_->callback_();

EXPECT_CALL(*router_.retry_state_, shouldRetryHeaders(_, _)).WillOnce(Return(RetryStatus::No));
Expand Down Expand Up @@ -6119,6 +6119,48 @@ TEST_F(RouterTest, PostHttpUpstream) {
router_.onDestroy();
}

TEST_F(RouterTest, SetDynamicMaxStreamDuration) {
NiceMock<Http::MockRequestEncoder> encoder1;
EXPECT_CALL(cm_.thread_local_cluster_.conn_pool_, newStream(_, _))
.WillOnce(Invoke([&](Http::ResponseDecoder&, Http::ConnectionPool::Callbacks& callbacks)
-> Http::ConnectionPool::Cancellable* {
callbacks.onPoolReady(encoder1, cm_.thread_local_cluster_.conn_pool_.host_,
upstream_stream_info_, Http::Protocol::Http10);
return nullptr;
}));
expectMaxStreamDurationTimerCreate(std::chrono::milliseconds(500));

Http::TestRequestHeaderMapImpl headers{{"x-envoy-upstream-stream-duration-ms", "500"}};

HttpTestUtility::addDefaultHeaders(headers);
router_.decodeHeaders(headers, false);
max_stream_duration_timer_->invokeCallback();

router_.onDestroy();
EXPECT_TRUE(verifyHostUpstreamStats(0, 0));
}

TEST_F(RouterTest, NotSetDynamicMaxStreamDurationIfZero) {
NiceMock<Http::MockRequestEncoder> encoder1;
EXPECT_CALL(cm_.thread_local_cluster_.conn_pool_, newStream(_, _))
.WillOnce(Invoke([&](Http::ResponseDecoder&, Http::ConnectionPool::Callbacks& callbacks)
-> Http::ConnectionPool::Cancellable* {
callbacks.onPoolReady(encoder1, cm_.thread_local_cluster_.conn_pool_.host_,
upstream_stream_info_, Http::Protocol::Http10);
return nullptr;
}));

// The timer will not be created.
EXPECT_CALL(callbacks_.dispatcher_, createTimer_).Times(0);

Http::TestRequestHeaderMapImpl headers{{"x-envoy-upstream-stream-duration-ms", "0"}};
HttpTestUtility::addDefaultHeaders(headers);
router_.decodeHeaders(headers, false);

router_.onDestroy();
EXPECT_TRUE(verifyHostUpstreamStats(0, 0));
}

// Test that request/response header/body sizes are properly recorded.
TEST_F(RouterTest, RequestResponseSize) { testRequestResponseSize(false); }

Expand Down
5 changes: 3 additions & 2 deletions test/common/router/router_test_base.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace Envoy {
namespace Router {

using ::testing::AnyNumber;
using ::testing::Eq;
using ::testing::ReturnRef;

RouterTestBase::RouterTestBase(bool start_child_span, bool suppress_envoy_headers,
Expand Down Expand Up @@ -50,9 +51,9 @@ void RouterTestBase::expectPerTryTimerCreate() {
EXPECT_CALL(*per_try_timeout_, disableTimer());
}

void RouterTestBase::expectMaxStreamDurationTimerCreate() {
void RouterTestBase::expectMaxStreamDurationTimerCreate(std::chrono::milliseconds duration_msec) {
max_stream_duration_timer_ = new Event::MockTimer(&callbacks_.dispatcher_);
EXPECT_CALL(*max_stream_duration_timer_, enableTimer(_, _));
EXPECT_CALL(*max_stream_duration_timer_, enableTimer(Eq(duration_msec), _));
EXPECT_CALL(*max_stream_duration_timer_, disableTimer());
}

Expand Down
4 changes: 3 additions & 1 deletion test/common/router/router_test_base.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#pragma once

#include <chrono>

#include "source/common/http/context_impl.h"
#include "source/common/router/router.h"
#include "source/common/stream_info/uint32_accessor_impl.h"
Expand Down Expand Up @@ -56,7 +58,7 @@ class RouterTestBase : public testing::Test {

void expectResponseTimerCreate();
void expectPerTryTimerCreate();
void expectMaxStreamDurationTimerCreate();
void expectMaxStreamDurationTimerCreate(std::chrono::milliseconds duration_msec);
AssertionResult verifyHostUpstreamStats(uint64_t success, uint64_t error);
void verifyMetadataMatchCriteriaFromRequest(bool route_entry_has_match);
void verifyAttemptCountInRequestBasic(bool set_include_attempt_count_in_request,
Expand Down
Loading