Skip to content

Commit 1e8c635

Browse files
authored
Merge pull request #628 from craigmiskell-gitlab/handle-rate-limiting-responses
Respect HTTP 429 and Retry-After header
2 parents 06946d8 + 8256d97 commit 1e8c635

2 files changed

Lines changed: 85 additions & 1 deletion

File tree

lib/gitlab/request.rb

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,18 @@ def self.decode(response)
4848
params[:headers].merge!(authorization_header)
4949
end
5050

51-
validate self.class.send(method, endpoint + path, params)
51+
retries_left = params[:ratelimit_retries] || 3
52+
begin
53+
response = self.class.send(method, endpoint + path, params)
54+
validate response
55+
rescue Gitlab::Error::TooManyRequests => e
56+
retries_left -= 1
57+
raise e if retries_left.zero?
58+
59+
wait_time = response.headers['Retry-After'] || 2
60+
sleep(wait_time.to_i)
61+
retry
62+
end
5263
end
5364
end
5465

spec/gitlab/request_spec.rb

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,77 @@
114114
expect(@request.send(:authorization_header)).to eq('Authorization' => 'Bearer 3225e2804d31fea13fc41fc83bffef00cfaedc463118646b154acc6f94747603')
115115
end
116116
end
117+
118+
describe 'ratelimiting' do
119+
before do
120+
@request.private_token = 'token'
121+
@request.endpoint = 'https://example.com/api/v4'
122+
@rpath = "#{@request.endpoint}/version"
123+
124+
allow(@request).to receive(:httparty)
125+
end
126+
127+
it 'tries 3 times when ratelimited by default' do
128+
stub_request(:get, @rpath)
129+
.to_return(
130+
status: 429,
131+
headers: { 'Retry-After' => 1 }
132+
)
133+
134+
expect do
135+
@request.get('/version')
136+
end.to raise_error(Gitlab::Error::TooManyRequests)
137+
138+
expect(a_request(:get, @rpath).with(headers: {
139+
'PRIVATE_TOKEN' => 'token'
140+
}.merge(described_class.headers))).to have_been_made.times(3)
141+
end
142+
143+
it 'tries 4 times when ratelimited with option' do
144+
stub_request(:get, @rpath)
145+
.to_return(
146+
status: 429,
147+
headers: { 'Retry-After' => 1 }
148+
)
149+
expect do
150+
@request.get('/version', { ratelimit_retries: 4 })
151+
end.to raise_error(Gitlab::Error::TooManyRequests)
152+
153+
expect(a_request(:get, @rpath).with(headers: {
154+
'PRIVATE_TOKEN' => 'token'
155+
}.merge(described_class.headers))).to have_been_made.times(4)
156+
end
157+
158+
it 'handles one retry then success' do
159+
stub_request(:get, @rpath)
160+
.to_return(
161+
status: 429,
162+
headers: { 'Retry-After' => 1 }
163+
).times(1).then
164+
.to_return(
165+
status: 200
166+
).times(1)
167+
168+
@request.get('/version')
169+
170+
expect(a_request(:get, @rpath).with(headers: {
171+
'PRIVATE_TOKEN' => 'token'
172+
}.merge(described_class.headers))).to have_been_made.times(2)
173+
end
174+
175+
it 'survives a 429 with no Retry-After header' do
176+
stub_request(:get, @rpath)
177+
.to_return(
178+
status: 429
179+
)
180+
181+
expect do
182+
@request.get('/version')
183+
end.to raise_error(Gitlab::Error::TooManyRequests)
184+
185+
expect(a_request(:get, @rpath).with(headers: {
186+
'PRIVATE_TOKEN' => 'token'
187+
}.merge(described_class.headers))).to have_been_made.times(3)
188+
end
189+
end
117190
end

0 commit comments

Comments
 (0)