Don't crash when a token refresh fails without an HTTP response - #56
Merged
Conversation
oauth2 raises OAuth2::Error.new({...}) with a plain Hash when the
failure never reached the token endpoint — most notably when the stored
token has no refresh token. Error#response then returns that Hash, and
the rescue's e.response.response.env died with NoMethodError instead of
surfacing anything useful.
Only map through Booqable::Error.from_response when the error actually
wraps an HTTP response; otherwise re-raise the OAuth2::Error as-is —
it's a config/data problem, not an API response, so inventing a
Booqable HTTP error for it would mislead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
respond_to?(:response) only proves the reader exists; an error shape whose #response returns nil (or a response without an env) would still crash on .env. In oauth2 2.0.24 that shape is unreachable - every response-carrying Error wraps an OAuth2::Response built from a completed Faraday response - but the guard costs a safe-navigation operator and stops depending on gem internals staying that way. Extract the inner HTTP response first and map only when it has an env; anything else re-raises the original OAuth2::Error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The exact wording is the oauth2 gems and may change across versions; the class plus a /refresh_token/ match still pins that the failure is about the missing refresh token. No structured attribute exists to assert instead: the gem raises with a symbol-keyed Hash while Error#initialize reads string keys, so e.code is nil for this shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.
Suppressed comments (3)
lib/booqable/middleware/auth/oauth.rb:107
- The YARD
@raisetag is now inaccurate:refresh_token!can re-raiseOAuth2::Errorwhen there is no HTTP response/env to map. Update the docstring to includeOAuth2::Error(and/or clarify the conditional behavior) so callers have an accurate contract.
# Booqable errors for consistent error handling. OAuth2 errors raised
# without an HTTP response (e.g. the stored token has no refresh
# token, so the error carries a plain Hash) propagate as-is — there
# is no response to map to a Booqable error.
#
# @return [OAuth2::AccessToken] The new access token
# @raise [Booqable::Error] For OAuth-related errors
spec/booqable/oauth_client_spec.rb:477
- This expectation matches the exact exception instance, which is more brittle than necessary and couples the spec to the implementation detail that the same object is re-raised. Prefer asserting on the exception class (and optionally message) to keep the test resilient while still verifying behavior.
expect { middleware.call(env) }.to raise_error(oauth_error)
spec/booqable/client_spec.rb:584
- Matching a specific OAuth2 gem error message can be brittle across gem versions/locales. If feasible, prefer asserting primarily on the exception class (or a narrower, project-controlled signal) and only match message text if it's essential to the contract being tested.
expect { client.get("/orders") }
.to raise_error(OAuth2::Error, /A refresh_token is not available/)
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.
Suppressed comments (3)
lib/booqable/middleware/auth/oauth.rb:107
- The YARD tag
@raise [Booqable::Error]is now incomplete/inaccurate because the method can re-raiseOAuth2::Errorwhen there’s no HTTP response/env to map. Update the@raisedocumentation to includeOAuth2::Error(or adjust wording to reflect both possible exception types).
# Booqable errors for consistent error handling. OAuth2 errors raised
# without an HTTP response (e.g. the stored token has no refresh
# token, so the error carries a plain Hash) propagate as-is — there
# is no response to map to a Booqable error.
#
# @return [OAuth2::AccessToken] The new access token
# @raise [Booqable::Error] For OAuth-related errors
lib/booqable/middleware/auth/oauth.rb:118
- This extraction only handles the case where
e.responseresponds to#responseand yields an object with#env. Ife.responseis already an HTTP response object with#env(or otherwise structured differently), this will incorrectly re-raise instead of mapping toBooqable::Error. Consider supporting both shapes by preferringe.response.envwhen available, otherwise falling back toe.response.response.env, and only re-raising when neither exists.
http_response = e.response.response if e.response.respond_to?(:response)
raise unless http_response&.env
Booqable::Error.from_response(http_response.env)
spec/booqable/client_spec.rb:584
- Matching on
/refresh_token/makes the spec brittle across OAuth2 gem versions/localizations since error messages can change while behavior stays correct. If the intent is to ensure the error propagates asOAuth2::Error(and not a mappedBooqable::Error/ not aNoMethodError), consider asserting only the exception class (or asserting it’s not a Booqable error) rather than depending on the message text.
expect { client.get("/orders") }
.to raise_error(OAuth2::Error, /refresh_token/)
Booqable::Error.from_response only raises when the status maps to an error class, and only 4xx/5xx do. But oauth2 also raises on a 200 from the token endpoint whose body has no access_token - from_response then returned nil, refresh_token! returned nil, and the request carried on with a nil token, crashing later with an obscure NoMethodError on the Authorization header line. Re-raise the original OAuth2::Error when the response maps to nothing, mirroring the existing no-HTTP-response path: a refresh failure must never return. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.
Suppressed comments (3)
lib/booqable/middleware/auth/oauth.rb:122
- The current extraction only handles error responses shaped like
e.response.response.env. Ife.responseitself is already an HTTP response object that exposesenv(but does not respond toresponse), this will incorrectly skip mapping and re-raiseOAuth2::Error. Consider supporting both shapes by also acceptinge.responsewhen it responds toenv.
http_response = e.response.response if e.response.respond_to?(:response)
raise unless http_response&.env
Booqable::Error.from_response(http_response.env)
raise
lib/booqable/middleware/auth/oauth.rb:121
- This relies on
Booqable::Error.from_responseraising internally for mapped errors, and uses a bareraiseas a fallback. Making the control flow explicit (e.g., capturing a returned mapped error and raising it when present, otherwise re-raising the original) would clarify intent and avoid coupling tofrom_responseside effects.
Booqable::Error.from_response(http_response.env)
raise
spec/booqable/oauth_client_spec.rb:477
- Asserting
raise_error(oauth_error)depends on the exact exception instance being re-raised (which is currently true due toand_raise(oauth_error)). To make the test more resilient while still validating behavior, consider asserting on the exception class (and optionally message) rather than object identity.
expect { middleware.call(env) }.to raise_error(oauth_error)
pbalaban
approved these changes
Aug 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
refresh_token!'s rescue assumes everyOAuth2::Errorwraps an HTTP response:But oauth2 also raises
OAuth2::Error.new({...})with a plain Hash when the failure never reached the token endpoint — most notably when the stored token has no refresh token (access_token.rb:218in oauth2 2.0.24).Error#responsereturns whatever the error was constructed with, soe.response.responsedies with:Reproduced against oauth2 2.0.24. Not a regression — this path has existed as long as the rescue — but the refresh buffer from #55 widens exposure slightly: a refresh-token-less token inside the 60s window used to be sent as-is and now triggers a refresh attempt.