Skip to content

Commit 103fbef

Browse files
authored
router: fix a bug where internal redirect will hang up request or unexpected redirect (#44154) (#44304)
Commit Message: router: fix a bug where internal redirect will hang up request or unexpected redirect (#44154) Additional Description: Risk Level: Testing: Docs Changes: Release Notes: Platform Specific Features: [Optional Runtime guard:] [Optional Fixes #Issue] [Optional Fixes commit #PR or SHA] [Optional Deprecated:] [Optional [API Considerations](https://github.com/envoyproxy/envoy/blob/main/api/review_checklist.md):] Signed-off-by: wbpcode <wbphub@gmail.com>
1 parent 52f9a56 commit 103fbef

3 files changed

Lines changed: 100 additions & 127 deletions

File tree

changelogs/current.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ bug_fixes:
1717
Fixed a bug where dynamic module filter may result in a incomplete body being sent to upstream
1818
or downstream when some filters before or after the dynamic module filter in the chain
1919
buffered the body and the dynamic module filter did not.
20+
- area: http
21+
change: |
22+
Fixed a bug where the internal redirect logic may hang up a request when the request buffer is
23+
overflowed.
2024
2125
removed_config_or_runtime:
2226
# *Normally occurs at the end of the* :ref:`deprecation period <deprecated>`

source/common/router/router.cc

Lines changed: 11 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -992,38 +992,34 @@ Http::FilterDataStatus Filter::decodeData(Buffer::Instance& data, bool end_strea
992992

993993
const bool retry_enabled = retry_state_ && retry_state_->enabled();
994994
const bool redirect_enabled = route_entry_ && route_entry_->internalRedirectPolicy().enabled();
995-
const bool is_redirect_only = redirect_enabled && !retry_enabled;
996995
const uint64_t effective_buffer_limit = calculateEffectiveBufferLimit();
997996

998-
bool buffering = retry_enabled || redirect_enabled;
997+
bool buffering = (retry_enabled || redirect_enabled) && (!request_buffer_overflowed_);
999998

1000999
// Check if we would exceed buffer limits, regardless of current buffering state
10011000
// This ensures error details are set even if retry state was cleared due to upstream reset.
10021001
const bool would_exceed_buffer =
10031002
(getLength(callbacks_->decodingBuffer()) + data.length() > effective_buffer_limit);
10041003

1005-
// Handle retry buffer overflow, excluding redirect-only scenarios.
1006-
// For redirect scenarios, buffer overflow should only affect redirect processing, not initial
1007-
// request.
1008-
if (would_exceed_buffer && retry_enabled && !is_redirect_only && !request_buffer_overflowed_) {
1004+
// Handle buffer overflow.
1005+
if (buffering && would_exceed_buffer) {
10091006
ENVOY_LOG(debug,
10101007
"The request payload has at least {} bytes data which exceeds buffer limit {}. "
10111008
"Giving up on buffering.",
10121009
getLength(callbacks_->decodingBuffer()) + data.length(), effective_buffer_limit);
1013-
10141010
cluster_->trafficStats()->retry_or_shadow_abandoned_.inc();
10151011
retry_state_.reset();
1016-
ENVOY_LOG(debug, "retry or shadow overflow: retry_state_ reset, buffering set to false");
1012+
ENVOY_LOG(debug, "retry or redirect buffer overflow: skipping buffering");
10171013
buffering = false;
10181014
active_shadow_policies_.clear();
1015+
request_buffer_overflowed_ = true;
10191016

10201017
// Only send local reply and cleanup if we're in a retry waiting state (no active upstream
10211018
// requests). If there are active upstream requests, let the normal upstream failure handling
10221019
// take precedence.
10231020
if (upstream_requests_.empty()) {
1024-
request_buffer_overflowed_ = true;
1025-
ENVOY_LOG(debug,
1026-
"retry or shadow overflow: No upstream requests, resetting and calling cleanup()");
1021+
ENVOY_LOG(debug, "retry or redirect buffer overflow: No upstream requests, resetting and "
1022+
"calling cleanup()");
10271023
resetAll();
10281024
cleanup();
10291025
callbacks_->streamInfo().setResponseCodeDetails(
@@ -1034,25 +1030,12 @@ Http::FilterDataStatus Filter::decodeData(Buffer::Instance& data, bool end_strea
10341030
StreamInfo::ResponseCodeDetails::get().RequestPayloadExceededRetryBufferLimit);
10351031
return Http::FilterDataStatus::StopIterationNoBuffer;
10361032
} else {
1037-
ENVOY_LOG(debug, "retry or shadow overflow: Upstream requests exist, deferring to normal "
1038-
"upstream failure handling");
1033+
ENVOY_LOG(debug,
1034+
"retry or redirect buffer overflow: Upstream requests exist, deferring to normal "
1035+
"upstream failure handling");
10391036
}
10401037
}
10411038

1042-
// Handle redirect-only buffer overflow when retry/shadow is not active.
1043-
// For redirect scenarios, buffer overflow should only affect redirect processing, not initial
1044-
// request.
1045-
if (would_exceed_buffer && is_redirect_only && !request_buffer_overflowed_) {
1046-
ENVOY_LOG(debug,
1047-
"The request payload has at least {} bytes data which exceeds buffer limit {}. "
1048-
"Marking request as buffer overflowed to cancel internal redirects.",
1049-
getLength(callbacks_->decodingBuffer()) + data.length(), effective_buffer_limit);
1050-
1051-
// Set the flag to cancel internal redirect processing, but allow the request to proceed
1052-
// normally.
1053-
request_buffer_overflowed_ = true;
1054-
}
1055-
10561039
for (auto* shadow_stream : shadow_streams_) {
10571040
if (end_stream) {
10581041
shadow_stream->removeDestructorCallback();
@@ -1444,13 +1427,6 @@ void Filter::onUpstreamAbort(Http::Code code, StreamInfo::CoreResponseFlag respo
14441427
// Otherwise just reset the ongoing response.
14451428
callbacks_->streamInfo().setResponseFlag(response_flags);
14461429

1447-
// Check if buffer overflow occurred and override error details accordingly
1448-
if (request_buffer_overflowed_) {
1449-
code = Http::Code::InsufficientStorage;
1450-
body = "exceeded request buffer limit while retrying upstream";
1451-
details = StreamInfo::ResponseCodeDetails::get().RequestPayloadExceededRetryBufferLimit;
1452-
}
1453-
14541430
// This will destroy any created retry timers.
14551431
cleanup();
14561432
// sendLocalReply may instead reset the stream if downstream_response_started_ is true.
@@ -2008,8 +1984,7 @@ bool Filter::setupRedirect(const Http::ResponseHeaderMap& headers) {
20081984
const uint64_t status_code = Http::Utility::getResponseStatus(headers);
20091985

20101986
// Redirects are not supported for streaming requests yet.
2011-
if (downstream_end_stream_ && (!request_buffer_overflowed_ || !callbacks_->decodingBuffer()) &&
2012-
location != nullptr &&
1987+
if (downstream_end_stream_ && (!request_buffer_overflowed_) && location != nullptr &&
20131988
convertRequestHeadersForInternalRedirect(*downstream_headers_, headers, *location,
20141989
status_code) &&
20151990
callbacks_->recreateStream(&headers)) {

test/common/router/router_test.cc

Lines changed: 85 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -3240,45 +3240,6 @@ TEST_F(RouterTest, RetryRequestDuringBodyBufferLimitExceeded) {
32403240
EXPECT_CALL(callbacks_, decodingBuffer()).WillRepeatedly(Return(&decoding_buffer));
32413241
EXPECT_CALL(callbacks_, addDecodedData(_, true))
32423242
.WillRepeatedly(Invoke([&](Buffer::Instance& data, bool) { decoding_buffer.move(data); }));
3243-
EXPECT_CALL(callbacks_.route_->route_entry_, requestBodyBufferLimit()).WillOnce(Return(10));
3244-
3245-
NiceMock<Http::MockRequestEncoder> encoder1;
3246-
Http::ResponseDecoder* response_decoder = nullptr;
3247-
expectNewStreamWithImmediateEncoder(encoder1, &response_decoder, Http::Protocol::Http10);
3248-
3249-
Http::TestRequestHeaderMapImpl headers{
3250-
{"x-envoy-retry-on", "5xx"}, {"x-envoy-internal", "true"}, {"myheader", "present"}};
3251-
HttpTestUtility::addDefaultHeaders(headers);
3252-
router_->decodeHeaders(headers, false);
3253-
const std::string body1("body1");
3254-
Buffer::OwnedImpl buf1(body1);
3255-
EXPECT_CALL(*router_->retry_state_, enabled()).Times(2).WillRepeatedly(Return(true));
3256-
router_->decodeData(buf1, false);
3257-
3258-
router_->retry_state_->expectResetRetry();
3259-
encoder1.stream_.resetStream(Http::StreamResetReason::RemoteReset);
3260-
3261-
// Send additional 15 bytes - total 55 bytes, which should exceed request body buffer limit (50).
3262-
const std::string body2(15, 'y');
3263-
Buffer::OwnedImpl buf2(body2);
3264-
router_->decodeData(buf2, false);
3265-
3266-
EXPECT_EQ(callbacks_.details(), "request_payload_exceeded_retry_buffer_limit");
3267-
EXPECT_EQ(1U, cm_.thread_local_cluster_.cluster_.info_->stats_store_
3268-
.counter("retry_or_shadow_abandoned")
3269-
.value());
3270-
EXPECT_TRUE(verifyHostUpstreamStats(0, 1));
3271-
}
3272-
3273-
// Test that router uses request_body_buffer_limit when configured instead of
3274-
// per_request_buffer_limit.
3275-
TEST_F(RouterTest, RequestBodyBufferLimitExceeded) {
3276-
Buffer::OwnedImpl decoding_buffer;
3277-
EXPECT_CALL(callbacks_, decodingBuffer()).WillRepeatedly(Return(&decoding_buffer));
3278-
EXPECT_CALL(callbacks_, addDecodedData(_, true))
3279-
.WillRepeatedly(Invoke([&](Buffer::Instance& data, bool) { decoding_buffer.move(data); }));
3280-
// Configure a large request body buffer limit (50 bytes) but small request buffer limit (10
3281-
// bytes).
32823243
EXPECT_CALL(callbacks_.route_->route_entry_, requestBodyBufferLimit()).WillOnce(Return(50));
32833244

32843245
NiceMock<Http::MockRequestEncoder> encoder1;
@@ -3290,7 +3251,7 @@ TEST_F(RouterTest, RequestBodyBufferLimitExceeded) {
32903251
HttpTestUtility::addDefaultHeaders(headers);
32913252
router_->decodeHeaders(headers, false);
32923253

3293-
// Send 40 bytes - should be within request body buffer limit (50) but exceeds retry limit (10).
3254+
// Send 40 bytes - should be within request body buffer limit (50).
32943255
const std::string body1(40, 'x');
32953256
Buffer::OwnedImpl buf1(body1);
32963257
EXPECT_CALL(*router_->retry_state_, enabled()).Times(2).WillRepeatedly(Return(true));
@@ -3319,8 +3280,7 @@ TEST_F(RouterTest, BufferLimitLogicCase1RequestBodyBufferLimitSet) {
33193280
EXPECT_CALL(callbacks_, addDecodedData(_, true))
33203281
.WillRepeatedly(Invoke([&](Buffer::Instance& data, bool) { decoding_buffer.move(data); }));
33213282

3322-
// Case 1: request_body_buffer_limit=60, per_request_buffer_limit_bytes=20
3323-
// Should use request_body_buffer_limit = 60
3283+
// Use route level buffer limit.
33243284
EXPECT_CALL(callbacks_.route_->route_entry_, requestBodyBufferLimit()).WillRepeatedly(Return(60));
33253285

33263286
NiceMock<Http::MockRequestEncoder> encoder1;
@@ -3353,8 +3313,7 @@ TEST_F(RouterTest, BufferLimitLogicCase1RequestBodyBufferLimitSet) {
33533313
EXPECT_TRUE(verifyHostUpstreamStats(0, 1));
33543314
}
33553315

3356-
// When per_request_buffer_limit_bytes is set but request_body_buffer_limit is not set,
3357-
// we should use min(per_request_buffer_limit_bytes, per_connection_buffer_limit_bytes).
3316+
// Route level request buffer limit will override the connection buffer limit.
33583317
TEST_F(RouterTest, BufferLimitLogicCase2PerRequestSetRequestBodyNotSet) {
33593318
Buffer::OwnedImpl decoding_buffer;
33603319
EXPECT_CALL(callbacks_, decodingBuffer()).WillRepeatedly(Return(&decoding_buffer));
@@ -3363,8 +3322,8 @@ TEST_F(RouterTest, BufferLimitLogicCase2PerRequestSetRequestBodyNotSet) {
33633322
// Set up the connection buffer limit mock to return 40 as expected
33643323
EXPECT_CALL(callbacks_, bufferLimit()).WillRepeatedly(Return(40));
33653324

3366-
// Case 2: per_request_buffer_limit_bytes=20, request_body_buffer_limit=not set
3367-
// Should use min(20, connection_buffer_limit) = 20 (since connection limit is default 40)
3325+
// Route level request buffer limit is set to 20, which should override connection buffer limit
3326+
// of 40.
33683327
EXPECT_CALL(callbacks_.route_->route_entry_, requestBodyBufferLimit()).WillRepeatedly(Return(20));
33693328

33703329
NiceMock<Http::MockRequestEncoder> encoder1;
@@ -3397,51 +3356,6 @@ TEST_F(RouterTest, BufferLimitLogicCase2PerRequestSetRequestBodyNotSet) {
33973356
EXPECT_TRUE(verifyHostUpstreamStats(0, 1));
33983357
}
33993358

3400-
// Test that when connection limit is smaller than per_request limit,
3401-
// we use min(per_request_buffer_limit_bytes, per_connection_buffer_limit_bytes) = connection limit.
3402-
TEST_F(RouterTest, BufferLimitLogicCase2ConnectionLimitSmaller) {
3403-
Buffer::OwnedImpl decoding_buffer;
3404-
EXPECT_CALL(callbacks_, decodingBuffer()).WillRepeatedly(Return(&decoding_buffer));
3405-
EXPECT_CALL(callbacks_, addDecodedData(_, true))
3406-
.WillRepeatedly(Invoke([&](Buffer::Instance& data, bool) { decoding_buffer.move(data); }));
3407-
// Set up the connection buffer limit mock to return 40 as expected
3408-
EXPECT_CALL(callbacks_, bufferLimit()).WillRepeatedly(Return(40));
3409-
3410-
// Case 2: per_request_buffer_limit_bytes=50, request_body_buffer_limit=not set
3411-
// Should use min(50, connection_limit) = min(50, 40) = 40
3412-
// With consolidated approach, the effective limit should be 40
3413-
EXPECT_CALL(callbacks_.route_->route_entry_, requestBodyBufferLimit()).WillRepeatedly(Return(40));
3414-
3415-
NiceMock<Http::MockRequestEncoder> encoder1;
3416-
Http::ResponseDecoder* response_decoder = nullptr;
3417-
expectNewStreamWithImmediateEncoder(encoder1, &response_decoder, Http::Protocol::Http10);
3418-
3419-
Http::TestRequestHeaderMapImpl headers{{"x-envoy-retry-on", "5xx"}, {"x-envoy-internal", "true"}};
3420-
HttpTestUtility::addDefaultHeaders(headers);
3421-
router_->decodeHeaders(headers, false);
3422-
3423-
// Send initial data (35 bytes)
3424-
const std::string body1(35, 'x');
3425-
Buffer::OwnedImpl buf1(body1);
3426-
EXPECT_CALL(*router_->retry_state_, enabled()).Times(2).WillRepeatedly(Return(true));
3427-
router_->decodeData(buf1, false);
3428-
3429-
// Simulate upstream failure to trigger retry logic
3430-
router_->retry_state_->expectResetRetry();
3431-
encoder1.stream_.resetStream(Http::StreamResetReason::RemoteReset);
3432-
3433-
// Send additional data (10 bytes) - total 45 bytes should exceed connection limit of 40
3434-
const std::string body2(10, 'y');
3435-
Buffer::OwnedImpl buf2(body2);
3436-
router_->decodeData(buf2, false);
3437-
3438-
EXPECT_EQ(callbacks_.details(), "request_payload_exceeded_retry_buffer_limit");
3439-
EXPECT_EQ(1U, cm_.thread_local_cluster_.cluster_.info_->stats_store_
3440-
.counter("retry_or_shadow_abandoned")
3441-
.value());
3442-
EXPECT_TRUE(verifyHostUpstreamStats(0, 1));
3443-
}
3444-
34453359
// Test that when neither fields are set we use per_connection_buffer_limit_bytes.
34463360
TEST_F(RouterTest, BufferLimitLogicCase3NeitherFieldSet) {
34473361
Buffer::OwnedImpl decoding_buffer;
@@ -4097,9 +4011,54 @@ TEST_F(RouterTest, NoRetryWithBodyLimit) {
40974011
EXPECT_CALL(callbacks_, addDecodedData(_, _)).Times(0);
40984012
Buffer::OwnedImpl body("t");
40994013
router_->decodeData(body, false);
4014+
Buffer::OwnedImpl body2("t");
4015+
// Ensure subsequent chunks after overflow are also not buffered.
4016+
router_->decodeData(body2, false);
41004017
EXPECT_EQ(1U,
41014018
callbacks_.route_->virtual_host_->virtual_cluster_.stats().upstream_rq_total_.value());
41024019

4020+
EXPECT_EQ(1U, cm_.thread_local_cluster_.cluster_.info_->stats_store_
4021+
.counter("retry_or_shadow_abandoned")
4022+
.value());
4023+
4024+
Http::ResponseHeaderMapPtr response_headers(
4025+
new Http::TestResponseHeaderMapImpl{{":status", "200"}});
4026+
response_decoder->decodeHeaders(std::move(response_headers), true);
4027+
}
4028+
4029+
TEST_F(RouterTest, EnableRedirectAndRetryButNoRetryWithBodyLimit) {
4030+
TestScopedRuntime scoped_runtime;
4031+
scoped_runtime.mergeValues(
4032+
{{"envoy.reloadable_features.allow_multiplexed_upstream_half_close", "false"}});
4033+
4034+
recreateFilter();
4035+
NiceMock<Http::MockRequestEncoder> encoder1;
4036+
Http::ResponseDecoder* response_decoder = nullptr;
4037+
expectNewStreamWithImmediateEncoder(encoder1, &response_decoder, Http::Protocol::Http10);
4038+
4039+
// Enable redirects also. This feature will not be used but will affect buffer logic.
4040+
enableRedirects(3);
4041+
4042+
// Set a per route body limit which disallows any buffering.
4043+
EXPECT_CALL(callbacks_.route_->route_entry_, requestBodyBufferLimit()).WillOnce(Return(0));
4044+
Http::TestRequestHeaderMapImpl headers{{"x-envoy-retry-on", "5xx"}, {"x-envoy-internal", "true"}};
4045+
HttpTestUtility::addDefaultHeaders(headers);
4046+
router_->decodeHeaders(headers, false);
4047+
// Unlike RetryUpstreamReset above the data won't be buffered as the body exceeds the buffer limit
4048+
EXPECT_CALL(*router_->retry_state_, enabled()).WillOnce(Return(true));
4049+
EXPECT_CALL(callbacks_, addDecodedData(_, _)).Times(0);
4050+
Buffer::OwnedImpl body("t");
4051+
router_->decodeData(body, false);
4052+
Buffer::OwnedImpl body2("t");
4053+
// Ensure subsequent chunks after overflow are also not buffered.
4054+
router_->decodeData(body2, false);
4055+
EXPECT_EQ(1U,
4056+
callbacks_.route_->virtual_host_->virtual_cluster_.stats().upstream_rq_total_.value());
4057+
4058+
EXPECT_EQ(1U, cm_.thread_local_cluster_.cluster_.info_->stats_store_
4059+
.counter("retry_or_shadow_abandoned")
4060+
.value());
4061+
41034062
Http::ResponseHeaderMapPtr response_headers(
41044063
new Http::TestResponseHeaderMapImpl{{":status", "200"}});
41054064
response_decoder->decodeHeaders(std::move(response_headers), true);
@@ -5067,6 +5026,41 @@ TEST_F(RouterTest, InternalRedirectAcceptedWithRequestBody) {
50675026
->value());
50685027
}
50695028

5029+
TEST_F(RouterTest, InternalRedirectWithRequestBodyBufferOverflow) {
5030+
EXPECT_CALL(callbacks_.route_->route_entry_, requestBodyBufferLimit()).WillOnce(Return(10));
5031+
5032+
enableRedirects();
5033+
sendRequest(false);
5034+
5035+
EXPECT_CALL(callbacks_.dispatcher_, createTimer_);
5036+
5037+
Buffer::InstancePtr body_data(new Buffer::OwnedImpl("random_fake_data"));
5038+
// We will not buffer the body data because it exceeds the buffer limit. But note the initial
5039+
// request is still valid.
5040+
EXPECT_CALL(callbacks_, addDecodedData(_, _)).Times(0);
5041+
EXPECT_EQ(Http::FilterDataStatus::StopIterationNoBuffer, router_->decodeData(*body_data, true));
5042+
5043+
// No redirect because buffer overflowed.
5044+
EXPECT_CALL(callbacks_.route_->route_entry_.internal_redirect_policy_, responseHeadersToCopy())
5045+
.Times(0);
5046+
EXPECT_CALL(callbacks_.downstream_callbacks_, clearRouteCache()).Times(0);
5047+
EXPECT_CALL(callbacks_, recreateStream(_)).Times(0);
5048+
5049+
response_decoder_->decodeHeaders(std::move(redirect_headers_), false);
5050+
Buffer::OwnedImpl response_data("1234567890");
5051+
response_decoder_->decodeData(response_data, false);
5052+
5053+
router_->onDestroy();
5054+
EXPECT_FALSE(callbacks_.streamInfo().filterState()->hasDataWithName("num_internal_redirects"));
5055+
5056+
EXPECT_EQ(1U, cm_.thread_local_cluster_.cluster_.info_->stats_store_
5057+
.counter("retry_or_shadow_abandoned")
5058+
.value());
5059+
EXPECT_EQ(1U, cm_.thread_local_cluster_.cluster_.info_->stats_store_
5060+
.counter("upstream_internal_redirect_failed_total")
5061+
.value());
5062+
}
5063+
50705064
TEST_F(RouterTest, CrossSchemeRedirectRejectedByPolicy) {
50715065
enableRedirects();
50725066
sendRequest();

0 commit comments

Comments
 (0)