Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

Note: For changes to the API, see https://shopify.dev/changelog?filter=api
## Unreleased
- [#1467](https://github.com/Shopify/shopify-api-ruby/pull/1467) Add optional `open_timeout` and `read_timeout` to `ShopifyAPI::Context.setup`, applied to every API request. Unset, requests keep Net::HTTP's defaults.

## 16.3.0 (2026-08-04)
- [#1443](https://github.com/Shopify/shopify-api-ruby/pull/1443) Add `ShopifyAPI::Utils::ShopValidator` with `sanitize_shop_domain` and `sanitize!`.
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@ ShopifyAPI::Context.setup(
)
```

By default, API requests use Net::HTTP's timeouts (60 seconds per read). To fail faster when Shopify is slow to respond, set `open_timeout` and/or `read_timeout` in seconds:

```ruby
ShopifyAPI::Context.setup(
# other params...
open_timeout: 5,
read_timeout: 10,
)
```

A request that times out raises `Net::OpenTimeout` or `Net::ReadTimeout`. It is not retried.

### Performing OAuth

You need to go through OAuth as described [here](https://shopify.dev/docs/apps/auth/oauth) to create sessions for shops using your app.
Expand Down
3 changes: 3 additions & 0 deletions lib/shopify_api/clients/http_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ def request(request, response_as_struct: false)

headers = append_first_party_development_headers(headers, parsed_uri)

timeouts = { open_timeout: Context.open_timeout, read_timeout: Context.read_timeout }.compact

tries = 0
response = HttpResponse.new(code: 0, headers: {}, body: "")
while tries < request.tries
Expand All @@ -54,6 +56,7 @@ def request(request, response_as_struct: false)
headers: headers,
query: request.query,
body: request.body.class == Hash ? T.unsafe(request.body).to_json : request.body,
**timeouts,
), HTTParty::Response)

begin
Expand Down
14 changes: 13 additions & 1 deletion lib/shopify_api/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ class Context

@rest_resource_loader = T.let(nil, T.nilable(Zeitwerk::Loader))
@expiring_offline_access_tokens = T.let(false, T::Boolean)
@open_timeout = T.let(nil, T.nilable(T.any(Integer, Float)))
@read_timeout = T.let(nil, T.nilable(T.any(Integer, Float)))

class << self
extend T::Sig
Expand All @@ -49,6 +51,8 @@ class << self
response_as_struct: T.nilable(T::Boolean),
rest_disabled: T.nilable(T::Boolean),
expiring_offline_access_tokens: T.nilable(T::Boolean),
open_timeout: T.nilable(T.any(Integer, Float)),
read_timeout: T.nilable(T.any(Integer, Float)),
).void
end
def setup(
Expand All @@ -68,7 +72,9 @@ def setup(
api_host: nil,
response_as_struct: false,
rest_disabled: false,
expiring_offline_access_tokens: false
expiring_offline_access_tokens: false,
open_timeout: nil,
read_timeout: nil
)
unless ShopifyAPI::AdminVersions::SUPPORTED_ADMIN_VERSIONS.include?(api_version)
raise Errors::UnsupportedVersionError,
Expand All @@ -90,6 +96,8 @@ def setup(
@response_as_struct = response_as_struct
@rest_disabled = rest_disabled
@expiring_offline_access_tokens = T.must(expiring_offline_access_tokens)
@open_timeout = open_timeout
@read_timeout = read_timeout
@log_level = if valid_log_level?(log_level)
log_level.to_sym
else
Expand Down Expand Up @@ -159,6 +167,10 @@ def private?
sig { returns(T::Boolean) }
attr_reader :expiring_offline_access_tokens

# Seconds to wait for a connection to open, and for each read, on API requests. nil keeps Net::HTTP's default.
sig { returns(T.nilable(T.any(Integer, Float))) }
attr_reader :open_timeout, :read_timeout

sig { returns(T::Boolean) }
def embedded?
@is_embedded
Expand Down
28 changes: 28 additions & 0 deletions test/clients/http_client_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,34 @@ def test_response_as_struct
assert_equal("nested_value", response.body.key.nested_key)
end

def test_request_applies_timeouts_from_context
modify_context(open_timeout: 5, read_timeout: 10)
apply_simple_http_stub

Net::HTTP.any_instance.expects(:open_timeout=).with(5)
Net::HTTP.any_instance.expects(:read_timeout=).with(10)

verify_http_request
end

def test_request_keeps_net_http_timeouts_when_context_has_none
apply_simple_http_stub

Net::HTTP.any_instance.expects(:open_timeout=).never
Net::HTTP.any_instance.expects(:read_timeout=).never

verify_http_request
end

def test_request_does_not_retry_a_timeout
stub_request(@request.http_method, "https://#{@shop}#{@base_path}/#{@request.path}")
.with(query: @request.query)
.to_raise(Net::ReadTimeout).then
.to_return(body: @success_body.to_json, headers: @response_headers)

assert_raises(Net::ReadTimeout) { @client.request(@request) }
end

private

def simple_http_test(http_method)
Expand Down
26 changes: 26 additions & 0 deletions test/context_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,32 @@ def test_expiring_offline_access_tokens_can_be_configured
assert(ShopifyAPI::Context.expiring_offline_access_tokens)
end

def test_timeouts_default_to_nil
ShopifyAPI::Context.setup(
api_key: "test-key",
api_secret_key: "test-secret-key",
api_version: "2023-01",
is_private: true,
is_embedded: false,
)
assert_nil(ShopifyAPI::Context.open_timeout)
assert_nil(ShopifyAPI::Context.read_timeout)
end

def test_timeouts_can_be_configured
ShopifyAPI::Context.setup(
api_key: "test-key",
api_secret_key: "test-secret-key",
api_version: "2023-01",
is_private: true,
is_embedded: false,
open_timeout: 5,
read_timeout: 10.5,
)
assert_equal(5, ShopifyAPI::Context.open_timeout)
assert_equal(10.5, ShopifyAPI::Context.read_timeout)
end

def teardown
ShopifyAPI::Context.deactivate_session
end
Expand Down
8 changes: 7 additions & 1 deletion test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ def setup
response_as_struct: T.nilable(T::Boolean),
api_host: T.nilable(String),
expiring_offline_access_tokens: T.nilable(T::Boolean),
open_timeout: T.nilable(T.any(Integer, Float)),
read_timeout: T.nilable(T.any(Integer, Float)),
).void
end
def modify_context(
Expand All @@ -70,7 +72,9 @@ def modify_context(
old_api_secret_key: nil,
response_as_struct: nil,
api_host: nil,
expiring_offline_access_tokens: nil
expiring_offline_access_tokens: nil,
open_timeout: nil,
read_timeout: nil
)
ShopifyAPI::Context.setup(
api_key: api_key ? api_key : ShopifyAPI::Context.api_key,
Expand All @@ -93,6 +97,8 @@ def modify_context(
else
ShopifyAPI::Context.expiring_offline_access_tokens
end,
open_timeout: open_timeout || ShopifyAPI::Context.open_timeout,
read_timeout: read_timeout || ShopifyAPI::Context.read_timeout,
)
end
end
Expand Down
Loading