Skip to content

Commit

Permalink
Merge pull request #9 from maxprokopiev/rate-limit-reset-header-support
Browse files Browse the repository at this point in the history
Add support for the RateLimit-Reset header
  • Loading branch information
olleolleolle authored Jun 7, 2022
2 parents 4a5426c + df7f0f3 commit 41e5500
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 20 deletions.
9 changes: 7 additions & 2 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2022-01-04 18:53:25 UTC using RuboCop version 1.23.0.
# on 2022-06-07 14:20:20 UTC using RuboCop version 1.21.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
Expand All @@ -17,6 +17,11 @@ Lint/AmbiguousBlockAssociation:
Metrics/AbcSize:
Max: 26

# Offense count: 1
# Configuration parameters: CountComments, CountAsOne.
Metrics/ClassLength:
Max: 105

# Offense count: 2
# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods.
Metrics/MethodLength:
Expand Down Expand Up @@ -49,7 +54,7 @@ RSpec/InstanceVariable:
RSpec/MultipleExpectations:
Max: 3

# Offense count: 9
# Offense count: 11
# Configuration parameters: AllowSubject.
RSpec/MultipleMemoizedHelpers:
Max: 9
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,14 @@ retry_options = {
}
```

#### Automatically handle the `Retry-After` header
#### Automatically handle the `Retry-After` and `RateLimit-Reset` headers

Some APIs, like the [Slack API](https://api.slack.com/docs/rate-limits), will inform you when you reach their API limits by replying with a response status code of `429` and a response header of `Retry-After` containing a time in seconds. You should then only retry querying after the amount of time provided by the `Retry-After` header, otherwise you won't get a response.
Some APIs, like the [Slack API](https://api.slack.com/docs/rate-limits), will inform you when you reach their API limits by replying with a response status code of `429`
and a response header of `Retry-After` containing a time in seconds. You should then only retry querying after the amount of time provided by the `Retry-After` header,
otherwise you won't get a response. Other APIs communicate their rate limits via the [RateLimit-xxx](https://tools.ietf.org/id/draft-polli-ratelimit-headers-00.html#rfc.section.3.3) headers
where `RateLimit-Reset` behaves similarly to the `Retry-After`.

You can automatically handle this and have Faraday pause and retry for the right amount of time by including the `429` status code in the retry statuses list:
You can automatically handle both headers and have Faraday pause and retry for the right amount of time by including the `429` status code in the retry statuses list:

```ruby
retry_options = {
Expand Down
36 changes: 23 additions & 13 deletions lib/faraday/retry/middleware.rb
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ def initialize(app, options = nil)
end

def calculate_sleep_amount(retries, env)
retry_after = calculate_retry_after(env)
retry_after = [calculate_retry_after(env), calculate_rate_limit_reset(env)].compact.max
retry_interval = calculate_retry_interval(retries)

return if retry_after && retry_after > @options.max_interval
Expand Down Expand Up @@ -212,21 +212,16 @@ def rewind_files(body)
end
end

# RFC for RateLimit Header Fields for HTTP:
# https://tools.ietf.org/id/draft-polli-ratelimit-headers-00.html#rfc.section.3.3
def calculate_rate_limit_reset(env)
parse_retry_header(env, 'RateLimit-Reset')
end

# MDN spec for Retry-After header:
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
def calculate_retry_after(env)
response_headers = env[:response_headers]
return unless response_headers

retry_after_value = env[:response_headers]['Retry-After']

# Try to parse date from the header value
begin
datetime = DateTime.rfc2822(retry_after_value)
datetime.to_time - Time.now.utc
rescue ArgumentError
retry_after_value.to_f
end
parse_retry_header(env, 'Retry-After')
end

def calculate_retry_interval(retries)
Expand All @@ -239,6 +234,21 @@ def calculate_retry_interval(retries)

current_interval + random_interval
end

def parse_retry_header(env, header)
response_headers = env[:response_headers]
return unless response_headers

retry_after_value = env[:response_headers][header]

# Try to parse date from the header value
begin
datetime = DateTime.rfc2822(retry_after_value)
datetime.to_time - Time.now.utc
rescue ArgumentError
retry_after_value.to_f
end
end
end
end
end
18 changes: 16 additions & 2 deletions spec/faraday/retry/middleware_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -245,8 +245,15 @@
conn.get('/unstable')
end

context 'when retry_after bigger than interval' do
let(:headers) { { 'Retry-After' => '0.5' } }
context 'when Retry-After bigger than RateLimit-Reset' do
let(:headers) { { 'Retry-After' => '0.5', 'RateLimit-Reset' => '0.1' } }
let(:options) { [{ max: 1, interval: 0.1, retry_statuses: 504 }] }

it { expect(elapsed).to be > 0.5 }
end

context 'when RateLimit-Reset bigger than Retry-After' do
let(:headers) { { 'Retry-After' => '0.1', 'RateLimit-Reset' => '0.5' } }
let(:options) { [{ max: 1, interval: 0.1, retry_statuses: 504 }] }

it { expect(elapsed).to be > 0.5 }
Expand All @@ -259,6 +266,13 @@
it { expect(elapsed).to be > 0.2 }
end

context 'when RateLimit-Reset is a timestamp' do
let(:headers) { { 'Retry-After' => '0.1', 'RateLimit-Reset' => (Time.now.utc + 2).strftime('%a, %d %b %Y %H:%M:%S GMT') } }
let(:options) { [{ max: 1, interval: 0.1, retry_statuses: 504 }] }

it { expect(elapsed).to be > 1 }
end

context 'when retry_after is a timestamp' do
let(:headers) { { 'Retry-After' => (Time.now.utc + 2).strftime('%a, %d %b %Y %H:%M:%S GMT') } }
let(:options) { [{ max: 1, interval: 0.1, retry_statuses: 504 }] }
Expand Down

0 comments on commit 41e5500

Please sign in to comment.