Skip to content

Commit 954bd04

Browse files
committed
Set the 'X-RateLimit-Reset' header
The 'X-RateLimit-Reset' header will be set to the number of seconds until the current throttle expires.
1 parent bc5ae7e commit 954bd04

File tree

3 files changed

+36
-3
lines changed

3 files changed

+36
-3
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# rack-attack-rate-limit changelog
22

3+
## master
4+
5+
* Set the 'X-RateLimit-Reset' header to the number of seconds until the current throttle period expires.
6+
37
## 1.1.0
48

59
* Add support for multiple throttles.
@@ -13,4 +17,4 @@
1317

1418
## 0.1.0
1519

16-
* Initial release.
20+
* Initial release.

lib/rack/attack/rate-limit.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ def add_rate_limit_headers!(headers, env)
5353
throttle_data = throttle_data_closest_to_limit(env)
5454
headers['X-RateLimit-Limit'] = rate_limit_limit(throttle_data).to_s
5555
headers['X-RateLimit-Remaining'] = rate_limit_remaining(throttle_data).to_s
56+
headers['X-RateLimit-Reset'] = rate_limit_reset(throttle_data).to_s
5657
headers
5758
end
5859

@@ -76,6 +77,16 @@ def rate_limit_remaining(throttle_data)
7677
rate_limit_limit(throttle_data) - throttle_data[:count]
7778
end
7879

80+
# RateLimit seconds until the current period expires from Rack::Attack
81+
#
82+
# env - Hash
83+
#
84+
# Returns Fixnum
85+
def rate_limit_reset(throttle_data)
86+
throttle_period = throttle_data[:period]
87+
throttle_period - (Time.now.to_i % throttle_period)
88+
end
89+
7990
# Rate Limit available method for Rack::Attack provider
8091
# Checks that at least one of the keys provided by the user are in the rack.attack.throttle_data env hash key
8192
#

spec/rack/attack/rate-limit_spec.rb

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
it 'should not create RateLimit headers' do
2626
last_response.header.key?('X-RateLimit-Limit').should be false
2727
last_response.header.key?('X-RateLimit-Remaining').should be false
28+
last_response.header.key?('X-RateLimit-Reset').should be false
2829
end
2930

3031
end
@@ -36,16 +37,18 @@
3637

3738
let(:request_limit) { (1..10_000).to_a.sample }
3839
let(:request_count) { (1..(request_limit - 10)).to_a.sample }
40+
let(:request_period) { rand(60..3600) }
3941

4042
context 'one throttle only' do
4143

4244
let(:rack_attack_throttle_data) do
43-
{ "#{throttle_one}" => { count: request_count, limit: request_limit } }
45+
{ "#{throttle_one}" => { count: request_count, limit: request_limit, period: request_period} }
4446
end
4547

4648
it 'should include RateLimit headers' do
4749
last_response.header.key?('X-RateLimit-Limit').should be true
4850
last_response.header.key?('X-RateLimit-Remaining').should be true
51+
last_response.header.key?('X-RateLimit-Reset').should be true
4952
end
5053

5154
it 'should return correct rate limit in header' do
@@ -55,6 +58,12 @@
5558
it 'should return correct remaining calls in header' do
5659
last_response.header['X-RateLimit-Remaining'].to_i.should eq(request_limit - request_count)
5760
end
61+
62+
it 'should returns the number of seconds remaining in the current throttle period' do
63+
current_epoch_time = Time.now.to_i
64+
seconds_until_next_period = request_period - (current_epoch_time % request_period)
65+
last_response.header['X-RateLimit-Reset'].to_i.should eq(seconds_until_next_period)
66+
end
5867
end
5968

6069
context 'multiple throttles' do
@@ -69,17 +78,20 @@
6978

7079
let(:request_limits) { 3.times.map { (1..10_000).to_a.sample } }
7180
let(:request_counts) { 3.times.map { |index| (1..(request_limits[index] - 10)).to_a.sample } }
81+
let(:request_periods) { 3.times.map { rand(60..3600) } }
7282

7383
let(:rack_attack_throttle_data) do
7484
data = {}
7585
[throttle_one, throttle_two, throttle_three].each_with_index do |thr, thr_index|
76-
data["#{thr}"] = { count: request_counts[thr_index], limit: request_limits[thr_index] }
86+
data["#{thr}"] = { count: request_counts[thr_index], limit: request_limits[thr_index], period: request_periods[thr_index] }
7787
end
7888
data
7989
end
90+
8091
it 'should include RateLimit headers' do
8192
last_response.header.key?('X-RateLimit-Limit').should be true
8293
last_response.header.key?('X-RateLimit-Remaining').should be true
94+
last_response.header.key?('X-RateLimit-Reset').should be true
8395
end
8496

8597
describe 'header values' do
@@ -95,6 +107,12 @@
95107
it 'should return correct remaining calls' do
96108
last_response.header['X-RateLimit-Remaining'].to_i.should eq(request_differences[min_index])
97109
end
110+
111+
it 'should return the correct number of seconds until the current throttled period expires' do
112+
current_epoch_time = Time.now.to_i
113+
seconds_until_next_period = request_periods[min_index] - (current_epoch_time % request_periods[min_index])
114+
last_response.header['X-RateLimit-Reset'].to_i.should eq(seconds_until_next_period)
115+
end
98116
end
99117
end
100118
end

0 commit comments

Comments
 (0)