diff --git a/CHANGELOG.md b/CHANGELOG.md index 782e14f7..2ac8bb64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Note: For changes to the API, see https://shopify.dev/changelog?filter=api ## Unreleased - [#1443](https://github.com/Shopify/shopify-api-ruby/pull/1443) Add `ShopifyAPI::Utils::ShopValidator` (module) with `sanitize_shop_domain` and `sanitize!`. - [#1443](https://github.com/Shopify/shopify-api-ruby/pull/1443) `ShopifyAPI::Auth::TokenExchange.exchange_token` always uses the session token's `dest` claim, instead of the `shop` parameter, that is now deprecated. It will show a deprecation warning and the argument will be removed in the next major version. +- [#1446](https://github.com/Shopify/shopify-api-ruby/pull/1446) `ShopifyAPI::Clients::HttpClient` requests now apply retry logic to 502/503/504 responses alongside 500s. ## 16.2.0 (2026-04-13) - [#1442](https://github.com/Shopify/shopify-api-ruby/pull/1442) Add support for 2026-04 API version diff --git a/docs/usage/rest.md b/docs/usage/rest.md index 4d29010b..4f1272e0 100644 --- a/docs/usage/rest.md +++ b/docs/usage/rest.md @@ -214,6 +214,20 @@ Each method can take the parameters outlined in the table below. **Note:** _These parameters can still be used in all methods regardless of if they are required._ +#### Automatic Retries + +When `tries` is greater than `1`, the client will automatically retry requests that receive one of the following HTTP status codes: + +- `429 Too Many Requests` +- `500 Internal Server Error` +- `502 Bad Gateway` +- `503 Service Unavailable` +- `504 Gateway Timeout` + +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. + +If all retries are exhausted, a `ShopifyAPI::Errors::MaxHttpRetriesExceededError` is raised. + #### Output ##### Success 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: diff --git a/lib/shopify_api/clients/http_client.rb b/lib/shopify_api/clients/http_client.rb index 6aeff505..7605eea8 100644 --- a/lib/shopify_api/clients/http_client.rb +++ b/lib/shopify_api/clients/http_client.rb @@ -80,7 +80,7 @@ def request(request, response_as_struct: false) error_message = serialized_error(response) - unless [429, 500].include?(response.code) + unless [429, 500, 502, 503, 504].include?(response.code) raise ShopifyAPI::Errors::HttpResponseError.new(response: response), error_message end @@ -91,7 +91,7 @@ def request(request, response_as_struct: false) "Exceeded maximum retry count of #{request.tries}. Last message: #{error_message}" end - if response.code == 500 || response.headers["retry-after"].nil? + if [500, 502, 503, 504].include?(response.code) || response.headers["retry-after"].nil? sleep(RETRY_WAIT_TIME) else sleep(T.must(response.headers["retry-after"])[0].to_i) diff --git a/test/clients/http_client_test.rb b/test/clients/http_client_test.rb index 3f2282d2..d15d9df0 100644 --- a/test/clients/http_client_test.rb +++ b/test/clients/http_client_test.rb @@ -240,6 +240,48 @@ def test_retry_internal_error verify_http_request end + def test_retry_bad_gateway + @request.tries = 2 + + @client.expects(:sleep).with(1).times(1) + + stub_request(@request.http_method, "https://#{@shop}#{@base_path}/#{@request.path}") + .with(body: @request.body.to_json, query: @request.query, headers: @expected_headers) + .to_return(body: { errors: "Bad gateway" }.to_json, headers: @response_headers, status: 502) + .times(1) + .then.to_return(body: @success_body.to_json, headers: @response_headers) + + verify_http_request + end + + def test_retry_service_unavailable + @request.tries = 2 + + @client.expects(:sleep).with(1).times(1) + + stub_request(@request.http_method, "https://#{@shop}#{@base_path}/#{@request.path}") + .with(body: @request.body.to_json, query: @request.query, headers: @expected_headers) + .to_return(body: { errors: "Service unavailable" }.to_json, headers: @response_headers, status: 503) + .times(1) + .then.to_return(body: @success_body.to_json, headers: @response_headers) + + verify_http_request + end + + def test_retry_gateway_timeout + @request.tries = 2 + + @client.expects(:sleep).with(1).times(1) + + stub_request(@request.http_method, "https://#{@shop}#{@base_path}/#{@request.path}") + .with(body: @request.body.to_json, query: @request.query, headers: @expected_headers) + .to_return(body: { errors: "Gateway timeout" }.to_json, headers: @response_headers, status: 504) + .times(1) + .then.to_return(body: @success_body.to_json, headers: @response_headers) + + verify_http_request + end + def test_retries_exceeded @request.tries = 3