Skip to content

Commit e42110a

Browse files
committed
Retry HttpClient requests on 502/503/504 too
Previously, HttpClient retries requests (when 'tries' was large enough) in two conditions: on 429 (obeying the retry-after header) and on 500 (retrying after 1 second). The 502 (Bad Gateway), 503 (Service Unavailable), and 504 (Gateway Timeout) statuses are all _infrastructure_ prompted conditions, but from the caller's perspective they are all equivalent to a 500: "the service failed to handle my request". Allowing retries to apply to these statuses is natural and appropriate. Add a small section to the docs describing the behavior of the 'tries' parameter in more detail, including the updated behavior.
1 parent 973f753 commit e42110a

3 files changed

Lines changed: 58 additions & 2 deletions

File tree

docs/usage/rest.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,20 @@ Each method can take the parameters outlined in the table below.
214214

215215
**Note:** _These parameters can still be used in all methods regardless of if they are required._
216216

217+
#### Automatic Retries
218+
219+
When `tries` is greater than `1`, the client will automatically retry requests that receive one of the following HTTP status codes:
220+
221+
- `429 Too Many Requests`
222+
- `500 Internal Server Error`
223+
- `502 Bad Gateway`
224+
- `503 Service Unavailable`
225+
- `504 Gateway Timeout`
226+
227+
For `429` responses, the sleep duration between retries is taken from the `Retry-After` response header if present. For all other retryable status codes, or if that header is not present, the client waits 1 second between attempts.
228+
229+
If all retries are exhausted, a `ShopifyAPI::Errors::MaxHttpRetriesExceededError` is raised.
230+
217231
#### Output
218232
##### Success
219233
If the request is successful these methods will all return a [`ShopifyAPI::Clients::HttpResponse`](https://github.com/Shopify/shopify-api-ruby/blob/main/lib/shopify_api/clients/http_response.rb) object, which has the following methods:

lib/shopify_api/clients/http_client.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def request(request, response_as_struct: false)
8080

8181
error_message = serialized_error(response)
8282

83-
unless [429, 500].include?(response.code)
83+
unless [429, 500, 502, 503, 504].include?(response.code)
8484
raise ShopifyAPI::Errors::HttpResponseError.new(response: response), error_message
8585
end
8686

@@ -91,7 +91,7 @@ def request(request, response_as_struct: false)
9191
"Exceeded maximum retry count of #{request.tries}. Last message: #{error_message}"
9292
end
9393

94-
if response.code == 500 || response.headers["retry-after"].nil?
94+
if [500, 502, 503, 504].include?(response.code) || response.headers["retry-after"].nil?
9595
sleep(RETRY_WAIT_TIME)
9696
else
9797
sleep(T.must(response.headers["retry-after"])[0].to_i)

test/clients/http_client_test.rb

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,48 @@ def test_retry_internal_error
240240
verify_http_request
241241
end
242242

243+
def test_retry_bad_gateway
244+
@request.tries = 2
245+
246+
@client.expects(:sleep).with(1).times(1)
247+
248+
stub_request(@request.http_method, "https://#{@shop}#{@base_path}/#{@request.path}")
249+
.with(body: @request.body.to_json, query: @request.query, headers: @expected_headers)
250+
.to_return(body: { errors: "Bad gateway" }.to_json, headers: @response_headers, status: 502)
251+
.times(1)
252+
.then.to_return(body: @success_body.to_json, headers: @response_headers)
253+
254+
verify_http_request
255+
end
256+
257+
def test_retry_service_unavailable
258+
@request.tries = 2
259+
260+
@client.expects(:sleep).with(1).times(1)
261+
262+
stub_request(@request.http_method, "https://#{@shop}#{@base_path}/#{@request.path}")
263+
.with(body: @request.body.to_json, query: @request.query, headers: @expected_headers)
264+
.to_return(body: { errors: "Service unavailable" }.to_json, headers: @response_headers, status: 503)
265+
.times(1)
266+
.then.to_return(body: @success_body.to_json, headers: @response_headers)
267+
268+
verify_http_request
269+
end
270+
271+
def test_retry_gateway_timeout
272+
@request.tries = 2
273+
274+
@client.expects(:sleep).with(1).times(1)
275+
276+
stub_request(@request.http_method, "https://#{@shop}#{@base_path}/#{@request.path}")
277+
.with(body: @request.body.to_json, query: @request.query, headers: @expected_headers)
278+
.to_return(body: { errors: "Gateway timeout" }.to_json, headers: @response_headers, status: 504)
279+
.times(1)
280+
.then.to_return(body: @success_body.to_json, headers: @response_headers)
281+
282+
verify_http_request
283+
end
284+
243285
def test_retries_exceeded
244286
@request.tries = 3
245287

0 commit comments

Comments
 (0)