Skip to content

Commit

Permalink
Automatically handle rate limiting errors
Browse files Browse the repository at this point in the history
We will sleep the prescribed number of seconds, then retry the request.

To disable, you can set retry_when_rate_limited to false.
  • Loading branch information
seven1m committed Feb 28, 2021
1 parent 6207f27 commit ad93a6a
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 4 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,17 @@ In the case of validation errors, the `message` is a summary string built from t
Alternatively, you may rescue `PCO::API::Errors::BaseError` and branch your code based on
the status code returned by calling `error.status`.

### TooManyRequests Error

By default, PCO::API::Endpoint will sleep and retry a request that fails with TooManyRequests due
to rate limiting. If you would rather catch and handle such errors yourself, you can disable this
behavior like this:

```ruby
api = PCO::API.new(...)
api.retry_when_rate_limited = false
```

## Copyright & License

Copyright Ministry Centered Technologies. Licensed MIT.
7 changes: 4 additions & 3 deletions lib/pco/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@

module PCO
module API
module_function
def new(**args)
Endpoint.new(**args)
class << self
def new(**args)
Endpoint.new(**args)
end
end
end
end
23 changes: 22 additions & 1 deletion lib/pco/api/endpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@ class Response < Hash
class Endpoint
attr_reader :url, :last_result

attr_accessor :retry_when_rate_limited

def initialize(url: URL, oauth_access_token: nil, basic_auth_token: nil, basic_auth_secret: nil, connection: nil)
@url = url
@oauth_access_token = oauth_access_token
@basic_auth_token = basic_auth_token
@basic_auth_secret = basic_auth_secret
@connection = connection || _build_connection
@cache = {}
@retry_when_rate_limited = true
end

def method_missing(method_name, *_args)
Expand All @@ -29,7 +32,7 @@ def [](id)
_build_endpoint(id.to_s)
end

def respond_to?(method_name)
def respond_to?(method_name, _include_all = false)
endpoint = _build_endpoint(method_name.to_s)
begin
endpoint.get
Expand All @@ -43,20 +46,26 @@ def respond_to?(method_name)
def get(params = {})
@last_result = @connection.get(@url, params)
_build_response(@last_result)
rescue Errors::TooManyRequests => e
_retry_after_timeout?(e) ? retry : raise
end

def post(body = {})
@last_result = @connection.post(@url) do |req|
req.body = _build_body(body)
end
_build_response(@last_result)
rescue Errors::TooManyRequests => e
_retry_after_timeout?(e) ? retry : raise
end

def patch(body = {})
@last_result = @connection.patch(@url) do |req|
req.body = _build_body(body)
end
_build_response(@last_result)
rescue Errors::TooManyRequests => e
_retry_after_timeout?(e) ? retry : raise
end

def delete
Expand All @@ -66,6 +75,8 @@ def delete
else
_build_response(@last_result)
end
rescue Errors::TooManyRequests => e
_retry_after_timeout?(e) ? retry : raise
end

private
Expand Down Expand Up @@ -135,6 +146,16 @@ def _build_connection
faraday.adapter :excon
end
end

def _retry_after_timeout?(e)
if @retry_when_rate_limited
secs = e.headers['Retry-After']
Kernel.sleep(secs ? secs.to_i : 1)
true
else
false
end
end
end
end
end
42 changes: 42 additions & 0 deletions spec/pco/api/endpoint_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,48 @@
}.to raise_error(PCO::API::Errors::ServerError)
end
end

context 'given a 429 error due to rate limiting' do
subject { base.people.v2 }

let(:result) do
{
'type' => 'Organization',
'id' => '1',
'name' => 'Ministry Centered Technologies',
'links' => {}
}
end

before do
stub_request(:get, 'https://api.planningcenteronline.com/people/v2')
.to_return([
{ status: 429, headers: { 'retry-after' => '2' } },
{ status: 200, body: { data: result }.to_json, headers: { 'Content-Type' => 'application/vnd.api+json' } }
])
end

context 'given retry_when_rate_limited is true' do
before do
subject.retry_when_rate_limited = true
end

it 'sleeps, then makes the call again' do
expect(Kernel).to receive(:sleep).with(2)
expect(subject.get).to be_a(Hash)
end
end

context 'given retry_when_rate_limited is false' do
before do
subject.retry_when_rate_limited = false
end

it 'raises the TooManyRequests error' do
expect { subject.get }.to raise_error(PCO::API::Errors::TooManyRequests)
end
end
end
end

describe '#post' do
Expand Down

0 comments on commit ad93a6a

Please sign in to comment.