Skip to content

Commit

Permalink
Respect HTTP 429 and Retry-After header
Browse files Browse the repository at this point in the history
When a 429 is received, sleep for the length of time indicated by Retry-After, then try again.

Try up to 3 times before giving up, although the count can be configured with :ratelimit_retries if you want more or less

If no Retry-After is sent by the server, defaults to 2 seconds (something is better than nothing). That should only happen with quite old and unsupported versions of GitLab
  • Loading branch information
craigmiskell-gitlab committed Oct 8, 2021
1 parent 06946d8 commit 8256d97
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 1 deletion.
13 changes: 12 additions & 1 deletion lib/gitlab/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,18 @@ def self.decode(response)
params[:headers].merge!(authorization_header)
end

validate self.class.send(method, endpoint + path, params)
retries_left = params[:ratelimit_retries] || 3
begin
response = self.class.send(method, endpoint + path, params)
validate response
rescue Gitlab::Error::TooManyRequests => e
retries_left -= 1
raise e if retries_left.zero?

wait_time = response.headers['Retry-After'] || 2
sleep(wait_time.to_i)
retry
end
end
end

Expand Down
73 changes: 73 additions & 0 deletions spec/gitlab/request_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,77 @@
expect(@request.send(:authorization_header)).to eq('Authorization' => 'Bearer 3225e2804d31fea13fc41fc83bffef00cfaedc463118646b154acc6f94747603')
end
end

describe 'ratelimiting' do
before do
@request.private_token = 'token'
@request.endpoint = 'https://example.com/api/v4'
@rpath = "#{@request.endpoint}/version"

allow(@request).to receive(:httparty)
end

it 'tries 3 times when ratelimited by default' do
stub_request(:get, @rpath)
.to_return(
status: 429,
headers: { 'Retry-After' => 1 }
)

expect do
@request.get('/version')
end.to raise_error(Gitlab::Error::TooManyRequests)

expect(a_request(:get, @rpath).with(headers: {
'PRIVATE_TOKEN' => 'token'
}.merge(described_class.headers))).to have_been_made.times(3)
end

it 'tries 4 times when ratelimited with option' do
stub_request(:get, @rpath)
.to_return(
status: 429,
headers: { 'Retry-After' => 1 }
)
expect do
@request.get('/version', { ratelimit_retries: 4 })
end.to raise_error(Gitlab::Error::TooManyRequests)

expect(a_request(:get, @rpath).with(headers: {
'PRIVATE_TOKEN' => 'token'
}.merge(described_class.headers))).to have_been_made.times(4)
end

it 'handles one retry then success' do
stub_request(:get, @rpath)
.to_return(
status: 429,
headers: { 'Retry-After' => 1 }
).times(1).then
.to_return(
status: 200
).times(1)

@request.get('/version')

expect(a_request(:get, @rpath).with(headers: {
'PRIVATE_TOKEN' => 'token'
}.merge(described_class.headers))).to have_been_made.times(2)
end

it 'survives a 429 with no Retry-After header' do
stub_request(:get, @rpath)
.to_return(
status: 429
)

expect do
@request.get('/version')
end.to raise_error(Gitlab::Error::TooManyRequests)

expect(a_request(:get, @rpath).with(headers: {
'PRIVATE_TOKEN' => 'token'
}.merge(described_class.headers))).to have_been_made.times(3)
end
end
end

0 comments on commit 8256d97

Please sign in to comment.