Skip to content

Commit a34c187

Browse files
committed
Allow to configure Retry-After header for default throttled_response handler
1 parent 0112405 commit a34c187

5 files changed

Lines changed: 37 additions & 8 deletions

File tree

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,11 @@ end
342342
While Rack::Attack's primary focus is minimizing harm from abusive clients, it
343343
can also be used to return rate limit data that's helpful for well-behaved clients.
344344

345+
If you want to return to user how many seconds to wait until he can start sending requests again, this can be done through enabling `Retry-After` header:
346+
```ruby
347+
Rack::Attack.throttled_response_retry_after_header = true
348+
```
349+
345350
Here's an example response that includes conventional `RateLimit-*` headers:
346351

347352
```ruby

lib/rack/attack.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ def clear!
6767
:blocklisted_response=,
6868
:throttled_response,
6969
:throttled_response=,
70+
:throttled_response_retry_after_header,
71+
:throttled_response_retry_after_header=,
7072
:clear_configuration,
7173
:safelists,
7274
:blocklists,

lib/rack/attack/configuration.rb

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ module Rack
44
class Attack
55
class Configuration
66
attr_reader :safelists, :blocklists, :throttles, :anonymous_blocklists, :anonymous_safelists
7-
attr_accessor :blocklisted_response, :throttled_response
7+
attr_accessor :blocklisted_response, :throttled_response, :throttled_response_retry_after_header
88

99
def initialize
1010
@safelists = {}
@@ -13,11 +13,18 @@ def initialize
1313
@tracks = {}
1414
@anonymous_blocklists = []
1515
@anonymous_safelists = []
16+
@throttled_response_retry_after_header = false
1617

1718
@blocklisted_response = lambda { |_env| [403, { 'Content-Type' => 'text/plain' }, ["Forbidden\n"]] }
1819
@throttled_response = lambda do |env|
19-
retry_after = (env['rack.attack.match_data'] || {})[:period]
20-
[429, { 'Content-Type' => 'text/plain', 'Retry-After' => retry_after.to_s }, ["Retry later\n"]]
20+
if throttled_response_retry_after_header
21+
match_data = env['rack.attack.match_data']
22+
now = match_data[:epoch_time]
23+
retry_after = match_data[:period] - (now % match_data[:period])
24+
[429, { 'Content-Type' => 'text/plain', 'Retry-After' => retry_after.to_s }, ["Retry later\n"]]
25+
else
26+
[429, { 'Content-Type' => 'text/plain' }, ["Retry later\n"]]
27+
end
2128
end
2229
end
2330

@@ -86,6 +93,7 @@ def clear_configuration
8693
@tracks = {}
8794
@anonymous_blocklists = []
8895
@anonymous_safelists = []
96+
@throttled_response_retry_after_header = false
8997
end
9098
end
9199
end

spec/acceptance/throttling_spec.rb

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
2121

2222
assert_equal 429, last_response.status
23-
assert_equal "60", last_response.headers["Retry-After"]
23+
assert_nil last_response.headers["Retry-After"]
2424
assert_equal "Retry later\n", last_response.body
2525

2626
get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
@@ -34,6 +34,24 @@
3434
end
3535
end
3636

37+
it "returns correct Retry-After header if enabled" do
38+
Rack::Attack.throttled_response_retry_after_header = true
39+
40+
Rack::Attack.throttle("by ip", limit: 1, period: 60) do |request|
41+
request.ip
42+
end
43+
44+
Timecop.freeze(Time.at(0)) do
45+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
46+
assert_equal 200, last_response.status
47+
end
48+
49+
Timecop.freeze(Time.at(25)) do
50+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
51+
assert_equal "35", last_response.headers["Retry-After"]
52+
end
53+
end
54+
3755
it "supports limit to be dynamic" do
3856
# Could be used to have different rate limits for authorized
3957
# vs general requests

spec/rack_attack_throttle_spec.rb

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,6 @@
5757

5858
_(last_request.env['rack.attack.match_discriminator']).must_equal('1.2.3.4')
5959
end
60-
61-
it 'should set a Retry-After header' do
62-
_(last_response.headers['Retry-After']).must_equal @period.to_s
63-
end
6460
end
6561
end
6662

0 commit comments

Comments
 (0)