mirror of
https://github.com/discourse/discourse.git
synced 2025-02-20 17:52:45 +08:00
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:
parent
0034cbda8a
commit
1f2f84a6df
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user