diff --git a/CHANGELOG.md b/CHANGELOG.md index 56f20cf2f..c3fc3ff01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ Note: For changes to the API, see https://shopify.dev/changelog?filter=api ## Unreleased +- [#1463](https://github.com/Shopify/shopify-api-ruby/pull/1463) Expose Shopify request ID on refreshed sessions and HTTP response errors. ## 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!`. diff --git a/lib/shopify_api/auth/refresh_token.rb b/lib/shopify_api/auth/refresh_token.rb index 8b91b054e..e1946c3aa 100644 --- a/lib/shopify_api/auth/refresh_token.rb +++ b/lib/shopify_api/auth/refresh_token.rb @@ -47,10 +47,12 @@ def refresh_access_token(shop:, refresh_token:) session_params = T.cast(response.body, T::Hash[String, T.untyped]).to_h - Session.from( + session = Session.from( shop: validated_shop, access_token_response: Oauth::AccessTokenResponse.from_hash(session_params), ) + session.request_id = response.request_id + session end end end diff --git a/lib/shopify_api/auth/session.rb b/lib/shopify_api/auth/session.rb index 0b7d1f9a3..5e287d746 100644 --- a/lib/shopify_api/auth/session.rb +++ b/lib/shopify_api/auth/session.rb @@ -36,6 +36,9 @@ class Session sig { returns(T.nilable(Time)) } attr_accessor :refresh_token_expires + sig { returns(T.nilable(String)) } + attr_accessor :request_id + sig { returns(T::Boolean) } def online? @is_online @@ -83,6 +86,7 @@ def initialize(shop:, id: nil, state: nil, access_token: "", scope: [], associat @shopify_session_id = shopify_session_id @refresh_token = refresh_token @refresh_token_expires = refresh_token_expires + @request_id = T.let(nil, T.nilable(String)) end class << self diff --git a/lib/shopify_api/clients/http_response.rb b/lib/shopify_api/clients/http_response.rb index e943b446a..cc1b788c1 100644 --- a/lib/shopify_api/clients/http_response.rb +++ b/lib/shopify_api/clients/http_response.rb @@ -51,6 +51,11 @@ def ok? code >= 200 && code <= 299 end + sig { returns(T.nilable(String)) } + def request_id + headers["x-request-id"]&.first + end + private sig { returns(T::Array[T.nilable(String)]) } diff --git a/lib/shopify_api/errors/http_response_error.rb b/lib/shopify_api/errors/http_response_error.rb index e4d912fa8..3c7ddcc22 100644 --- a/lib/shopify_api/errors/http_response_error.rb +++ b/lib/shopify_api/errors/http_response_error.rb @@ -18,6 +18,11 @@ def initialize(response:) @code = T.let(response.code, Integer) @response = response end + + sig { returns(T.nilable(String)) } + def request_id + response.request_id + end end end end diff --git a/test/auth/refresh_token_test.rb b/test/auth/refresh_token_test.rb index 6b1fd8310..4fd7dbd4a 100644 --- a/test/auth/refresh_token_test.rb +++ b/test/auth/refresh_token_test.rb @@ -67,6 +67,29 @@ def test_refresh_access_token_success assert_equal(expected_session, session) end + def test_refresh_access_token_exposes_request_id + request_id = "req-#{SecureRandom.hex(8)}" + + stub_request(:post, "https://#{@shop}/admin/oauth/access_token") + .with(body: @refresh_token_request) + .to_return( + body: @refresh_token_response.to_json, + headers: { + "Content-Type" => "application/json", + "x-request-id" => request_id, + }, + ) + + session = Time.stub(:now, @stubbed_time_now) do + ShopifyAPI::Auth::RefreshToken.refresh_access_token( + shop: @shop, + refresh_token: @refresh_token, + ) + end + + assert_equal(request_id, session.request_id) + end + def test_refresh_access_token_context_not_setup modify_context(api_key: "", api_secret_key: "", host: "") @@ -96,6 +119,32 @@ def test_refresh_access_token_unauthorized ) end end + + def test_refresh_access_token_error_exposes_request_id + request_id = "req-#{SecureRandom.hex(8)}" + + stub_request(:post, "https://#{@shop}/admin/oauth/access_token") + .with(body: @refresh_token_request) + .to_return( + status: 401, + body: { error: "unauthorized" }.to_json, + headers: { + "Content-Type" => "application/json", + "x-request-id" => request_id, + }, + ) + + ShopifyAPI::Context.logger.expects(:debug).with(regexp_matches(/Failed to refresh access token/)) + + error = assert_raises(ShopifyAPI::Errors::HttpResponseError) do + ShopifyAPI::Auth::RefreshToken.refresh_access_token( + shop: @shop, + refresh_token: @refresh_token, + ) + end + + assert_equal(request_id, error.request_id) + end end end end