Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions api/envoy/extensions/filters/http/ratelimit/v3/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ api_proto_package(
"//envoy/config/ratelimit/v3:pkg",
"//envoy/config/route/v3:pkg",
"//envoy/type/metadata/v3:pkg",
"//envoy/type/v3:pkg",
"@com_github_cncf_udpa//udpa/annotations:pkg",
],
)
11 changes: 10 additions & 1 deletion api/envoy/extensions/filters/http/ratelimit/v3/rate_limit.proto
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import "envoy/config/core/v3/extension.proto";
import "envoy/config/ratelimit/v3/rls.proto";
import "envoy/config/route/v3/route_components.proto";
import "envoy/type/metadata/v3/metadata.proto";
import "envoy/type/v3/http_status.proto";

import "google/protobuf/duration.proto";

Expand All @@ -23,7 +24,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE;
// Rate limit :ref:`configuration overview <config_http_filters_rate_limit>`.
// [#extension: envoy.filters.http.ratelimit]

// [#next-free-field: 10]
// [#next-free-field: 11]
message RateLimit {
option (udpa.annotations.versioning).previous_message_type =
"envoy.config.filter.http.rate_limit.v2.RateLimit";
Expand Down Expand Up @@ -111,6 +112,14 @@ message RateLimit {
// in case of rate limiting (i.e. 429 responses).
// Having this header not present potentially makes the request retriable.
bool disable_x_envoy_ratelimited_header = 9;

// This field allows for a custom HTTP response status code to the downstream client when
// the request has been rate limited.
// Defaults to 429 (TooManyRequests).
//
// .. note::
// If this is set to < 400, 429 will be used instead.
type.v3.HttpStatus rate_limited_status = 10;
}

// Global rate limiting :ref:`architecture overview <arch_overview_global_rate_limit>`.
Expand Down
4 changes: 2 additions & 2 deletions source/extensions/filters/http/ratelimit/ratelimit.cc
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ void Filter::complete(Filters::Common::RateLimit::LimitStatus status,
Http::CodeStats::ResponseStatInfo info{config_->scope(),
cluster_->statsScope(),
empty_stat_name,
enumToInt(Http::Code::TooManyRequests),
enumToInt(config_->rateLimitedStatus()),
true,
empty_stat_name,
empty_stat_name,
Expand Down Expand Up @@ -200,7 +200,7 @@ void Filter::complete(Filters::Common::RateLimit::LimitStatus status,
state_ = State::Responded;
callbacks_->streamInfo().setResponseFlag(StreamInfo::ResponseFlag::RateLimited);
callbacks_->sendLocalReply(
Http::Code::TooManyRequests, response_body,
config_->rateLimitedStatus(), response_body,
[this](Http::HeaderMap& headers) {
populateResponseHeaders(headers, /*from_local_reply=*/true);
},
Expand Down
13 changes: 12 additions & 1 deletion source/extensions/filters/http/ratelimit/ratelimit.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ class FilterConfig {
config.rate_limited_as_resource_exhausted()
? absl::make_optional(Grpc::Status::WellKnownGrpcStatus::ResourceExhausted)
: absl::nullopt),
http_context_(http_context), stat_names_(scope.symbolTable()) {}
http_context_(http_context), stat_names_(scope.symbolTable()),
rate_limited_status_(toErrorCode(config.rate_limited_status().code())) {}
const std::string& domain() const { return domain_; }
const LocalInfo::LocalInfo& localInfo() const { return local_info_; }
uint64_t stage() const { return stage_; }
Expand All @@ -70,6 +71,7 @@ class FilterConfig {
}
Http::Context& httpContext() { return http_context_; }
Filters::Common::RateLimit::StatNames& statNames() { return stat_names_; }
Http::Code rateLimitedStatus() { return rate_limited_status_; }

private:
static FilterRequestType stringToType(const std::string& request_type) {
Expand All @@ -83,6 +85,14 @@ class FilterConfig {
}
}

static Http::Code toErrorCode(uint64_t status) {
const auto code = static_cast<Http::Code>(status);
if (code >= Http::Code::BadRequest) {
return code;
}
return Http::Code::TooManyRequests;
}

const std::string domain_;
const uint64_t stage_;
const FilterRequestType request_type_;
Expand All @@ -95,6 +105,7 @@ class FilterConfig {
const absl::optional<Grpc::Status::GrpcStatus> rate_limited_grpc_status_;
Http::Context& http_context_;
Filters::Common::RateLimit::StatNames stat_names_;
const Http::Code rate_limited_status_;
};

using FilterConfigSharedPtr = std::shared_ptr<FilterConfig>;
Expand Down
49 changes: 49 additions & 0 deletions test/extensions/filters/http/ratelimit/ratelimit_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ class HttpRateLimitFilterTest : public testing::Test {
domain: foo
)EOF";

const std::string rate_limited_status_config_ = R"EOF(
domain: foo
rate_limited_status:
code: 503

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great to test the case of status code < 400

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok,i think it's good to add another test for this case?I will do it later,thanks~

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@soulxu I have add the test for case of status code < 400,PTAL,thanks~

)EOF";

Filters::Common::RateLimit::MockClient* client_;
NiceMock<Http::MockStreamDecoderFilterCallbacks> filter_callbacks_;
Stats::StatNamePool pool_{filter_callbacks_.clusterInfo()->statsScope().symbolTable()};
Expand All @@ -96,6 +102,8 @@ class HttpRateLimitFilterTest : public testing::Test {
Stats::StatName ratelimit_over_limit_{pool_.add("ratelimit.over_limit")};
Stats::StatName upstream_rq_4xx_{pool_.add("upstream_rq_4xx")};
Stats::StatName upstream_rq_429_{pool_.add("upstream_rq_429")};
Stats::StatName upstream_rq_5xx_{pool_.add("upstream_rq_5xx")};
Stats::StatName upstream_rq_503_{pool_.add("upstream_rq_503")};
Filters::Common::RateLimit::RequestCallbacks* request_callbacks_{};
Http::TestRequestHeaderMapImpl request_headers_;
Http::TestRequestTrailerMapImpl request_trailers_;
Expand Down Expand Up @@ -886,6 +894,47 @@ TEST_F(HttpRateLimitFilterTest, LimitResponseRuntimeDisabled) {
filter_callbacks_.clusterInfo()->statsScope().counterFromStatName(upstream_rq_429_).value());
}

TEST_F(HttpRateLimitFilterTest, LimitResponseWithRateLimitedStatus) {
SetUpTest(rate_limited_status_config_);
InSequence s;

EXPECT_CALL(route_rate_limit_, populateDescriptors(_, _, _, _))
.WillOnce(SetArgReferee<0>(descriptor_));
EXPECT_CALL(*client_, limit(_, _, _, _, _))
.WillOnce(
WithArgs<0>(Invoke([&](Filters::Common::RateLimit::RequestCallbacks& callbacks) -> void {
request_callbacks_ = &callbacks;
})));

EXPECT_EQ(Http::FilterHeadersStatus::StopIteration,
filter_->decodeHeaders(request_headers_, false));

EXPECT_CALL(filter_callbacks_.stream_info_,
setResponseFlag(StreamInfo::ResponseFlag::RateLimited));

Http::ResponseHeaderMapPtr h{new Http::TestResponseHeaderMapImpl()};
Http::TestResponseHeaderMapImpl response_headers{
{":status", "503"},
{"x-envoy-ratelimited", Http::Headers::get().EnvoyRateLimitedValues.True}};
EXPECT_CALL(filter_callbacks_, encodeHeaders_(HeaderMapEqualRef(&response_headers), true));
EXPECT_CALL(filter_callbacks_, continueDecoding()).Times(0);

request_callbacks_->complete(Filters::Common::RateLimit::LimitStatus::OverLimit, nullptr,
std::move(h), nullptr, "", nullptr);

EXPECT_EQ(1U, filter_callbacks_.clusterInfo()
->statsScope()
.counterFromStatName(ratelimit_over_limit_)
.value());
EXPECT_EQ(
1U,
filter_callbacks_.clusterInfo()->statsScope().counterFromStatName(upstream_rq_5xx_).value());
EXPECT_EQ(
1U,
filter_callbacks_.clusterInfo()->statsScope().counterFromStatName(upstream_rq_503_).value());
EXPECT_EQ("request_rate_limited", filter_callbacks_.details());
}

TEST_F(HttpRateLimitFilterTest, ResetDuringCall) {
SetUpTest(filter_config_);
InSequence s;
Expand Down