FIX: Add Retry-Header to rate limited responses (#11736)

It returned a 429 error code with a 'Retry-After' header if a
RateLimiter::LimitExceeded was raised and unhandled, but the header was
missing if the request was limited in the 'RequestTracker' middleware.
This commit is contained in:
Dan Ungureanu 2021-01-19 11:35:46 +02:00 committed by GitHub
parent 0034cbda8a
commit 1f2f84a6df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 39 additions and 25 deletions

View File

@ -20,8 +20,7 @@ class Middleware::RequestTracker
end end
def self.unregister_detailed_request_logger(callback) def self.unregister_detailed_request_logger(callback)
@@detailed_request_loggers.delete callback @@detailed_request_loggers.delete(callback)
if @@detailed_request_loggers.length == 0 if @@detailed_request_loggers.length == 0
@detailed_request_loggers = nil @detailed_request_loggers = nil
end end
@ -78,10 +77,9 @@ class Middleware::RequestTracker
ApplicationRequest.increment!(:http_4xx) ApplicationRequest.increment!(:http_4xx)
elsif status >= 300 elsif status >= 300
ApplicationRequest.increment!(:http_3xx) ApplicationRequest.increment!(:http_3xx)
elsif status >= 200 && status < 300 elsif status >= 200
ApplicationRequest.increment!(:http_2xx) ApplicationRequest.increment!(:http_2xx)
end end
end end
def self.get_data(env, result, timing) def self.get_data(env, result, timing)
@ -120,11 +118,11 @@ class Middleware::RequestTracker
if cache = headers["X-Discourse-Cached"] if cache = headers["X-Discourse-Cached"]
h[:cache] = cache h[:cache] = cache
end end
h h
end end
def log_request_info(env, result, info) def log_request_info(env, result, info)
# we got to skip this on error ... its just logging # we got to skip this on error ... its just logging
data = self.class.get_data(env, result, info) rescue nil data = self.class.get_data(env, result, info) rescue nil
@ -139,7 +137,6 @@ class Middleware::RequestTracker
log_later(data) log_later(data)
end end
end end
def self.populate_request_queue_seconds!(env) def self.populate_request_queue_seconds!(env)
@ -166,15 +163,20 @@ class Middleware::RequestTracker
request = Rack::Request.new(env) request = Rack::Request.new(env)
if rate_limit(request) if available_in = rate_limit(request)
result = [429, {}, ["Slow down, too many requests from this IP address"]] return [
return result 429,
{ "Retry-After" => available_in },
["Slow down, too many requests from this IP address"]
]
end end
env["discourse.request_tracker"] = self env["discourse.request_tracker"] = self
MethodProfiler.start MethodProfiler.start
result = @app.call(env) result = @app.call(env)
info = MethodProfiler.stop info = MethodProfiler.stop
# possibly transferred? # possibly transferred?
if info && (headers = result[1]) if info && (headers = result[1])
headers["X-Runtime"] = "%0.6f" % info[:total_duration] headers["X-Runtime"] = "%0.6f" % info[:total_duration]
@ -221,7 +223,6 @@ class Middleware::RequestTracker
end end
def rate_limit(request) def rate_limit(request)
if ( if (
GlobalSetting.max_reqs_per_ip_mode == "block" || GlobalSetting.max_reqs_per_ip_mode == "block" ||
GlobalSetting.max_reqs_per_ip_mode == "warn" || GlobalSetting.max_reqs_per_ip_mode == "warn" ||
@ -265,29 +266,38 @@ class Middleware::RequestTracker
request.env['DISCOURSE_RATE_LIMITERS'] = [limiter10, limiter60] request.env['DISCOURSE_RATE_LIMITERS'] = [limiter10, limiter60]
request.env['DISCOURSE_ASSET_RATE_LIMITERS'] = [limiter_assets10] request.env['DISCOURSE_ASSET_RATE_LIMITERS'] = [limiter_assets10]
warn = GlobalSetting.max_reqs_per_ip_mode == "warn" || warn = GlobalSetting.max_reqs_per_ip_mode == "warn" || GlobalSetting.max_reqs_per_ip_mode == "warn+block"
GlobalSetting.max_reqs_per_ip_mode == "warn+block"
if !limiter_assets10.can_perform? if !limiter_assets10.can_perform?
if warn if warn
Discourse.warn("Global asset IP rate limit exceeded for #{ip}: 10 second rate limit", uri: request.env["REQUEST_URI"]) Discourse.warn("Global asset IP rate limit exceeded for #{ip}: 10 second rate limit", uri: request.env["REQUEST_URI"])
end end
return !(GlobalSetting.max_reqs_per_ip_mode == "warn") if GlobalSetting.max_reqs_per_ip_mode != "warn"
return limiter_assets10.seconds_to_wait(Time.now.to_i)
else
return false
end
end end
type = 10
begin begin
type = 10
limiter10.performed! limiter10.performed!
type = 60 type = 60
limiter60.performed! limiter60.performed!
false false
rescue RateLimiter::LimitExceeded rescue RateLimiter::LimitExceeded => e
if warn if warn
Discourse.warn("Global IP rate limit exceeded for #{ip}: #{type} second rate limit", uri: request.env["REQUEST_URI"]) Discourse.warn("Global IP rate limit exceeded for #{ip}: #{type} second rate limit", uri: request.env["REQUEST_URI"])
!(GlobalSetting.max_reqs_per_ip_mode == "warn") if GlobalSetting.max_reqs_per_ip_mode != "warn"
e.available_in
else else
true false
end
else
e.available_in
end end
end end
end end

View File

@ -55,6 +55,10 @@ class RateLimiter
rate_unlimited? || is_under_limit? rate_unlimited? || is_under_limit?
end end
def seconds_to_wait(now)
@secs - age_of_oldest(now)
end
# reloader friendly # reloader friendly
unless defined? PERFORM_LUA unless defined? PERFORM_LUA
PERFORM_LUA = <<~LUA PERFORM_LUA = <<~LUA
@ -173,10 +177,6 @@ class RateLimiter
Discourse.redis.without_namespace Discourse.redis.without_namespace
end end
def seconds_to_wait(now)
@secs - age_of_oldest(now)
end
def age_of_oldest(now) def age_of_oldest(now)
# age of oldest event in buffer, in seconds # age of oldest event in buffer, in seconds
now - redis.lrange(prefixed_key, -1, -1).first.to_i now - redis.lrange(prefixed_key, -1, -1).first.to_i

View File

@ -237,10 +237,11 @@ describe Middleware::RequestTracker do
global_setting :max_reqs_per_ip_mode, 'warn+block' global_setting :max_reqs_per_ip_mode, 'warn+block'
status, _ = middleware.call(env) status, _ = middleware.call(env)
status, _ = middleware.call(env) status, headers = middleware.call(env)
expect(Rails.logger.warnings).to eq(1) expect(Rails.logger.warnings).to eq(1)
expect(status).to eq(429) expect(status).to eq(429)
expect(headers["Retry-After"]).to eq(10)
end end
it "does warn if rate limiter is enabled" do it "does warn if rate limiter is enabled" do
@ -267,13 +268,15 @@ describe Middleware::RequestTracker do
expect(status).to eq(200) expect(status).to eq(200)
status, _ = middleware.call(env1) status, _ = middleware.call(env1)
expect(status).to eq(200) expect(status).to eq(200)
status, _ = middleware.call(env1) status, headers = middleware.call(env1)
expect(status).to eq(429) expect(status).to eq(429)
expect(headers["Retry-After"]).to eq(10)
env2 = env("REMOTE_ADDR" => "1.1.1.1") env2 = env("REMOTE_ADDR" => "1.1.1.1")
status, _ = middleware.call(env2) status, headers = middleware.call(env2)
expect(status).to eq(429) expect(status).to eq(429)
expect(headers["Retry-After"]).to eq(10)
end end
it "does block if rate limiter is enabled" do it "does block if rate limiter is enabled" do
@ -286,8 +289,9 @@ describe Middleware::RequestTracker do
status, _ = middleware.call(env1) status, _ = middleware.call(env1)
expect(status).to eq(200) expect(status).to eq(200)
status, _ = middleware.call(env1) status, headers = middleware.call(env1)
expect(status).to eq(429) expect(status).to eq(429)
expect(headers["Retry-After"]).to eq(10)
status, _ = middleware.call(env2) status, _ = middleware.call(env2)
expect(status).to eq(200) expect(status).to eq(200)