we-promise/sure · error · Provider::Snaptrade::AuthenticationError
SnapTrade item has no refresh token
Error message
SnapTrade item has no refresh token
What it means
Inside refresh_access_token! (called under a DB row lock from ensure_fresh_token! when the access token is within TOKEN_EXPIRY_LEEWAY of expiry, or reactively after a 401), the item must have an oauth_refresh_token to mint a new access token. If the column is blank, AuthenticationError is raised; the method's rescue then marks the item status :requires_update, writes a DebugLogEntry (category provider_sync, provider snaptrade), and re-raises, so the sync aborts but the failure is visible in /settings/debug.
Source
Thrown at app/models/provider/snaptrade.rb:348
# that specific token (called from request_json). In that case we skip the HTTP refresh
# only if the DB row's access token has already changed since we made the failed request
# (i.e. another caller already won the race) -- an expiry-based freshness check would be
# wrong here since the server rejected a token we believed was still time-valid.
# When `previous_access_token` is absent, we're refreshing proactively (from
# ensure_fresh_token!) and skip only if the reloaded row is still time-fresh.
def refresh_access_token!(previous_access_token: nil)
snaptrade_item.with_lock do
snaptrade_item.reload
if previous_access_token.present?
next if snaptrade_item.oauth_access_token != previous_access_token
else
expires_at = snaptrade_item.oauth_token_expires_at
next if expires_at.present? && expires_at > TOKEN_EXPIRY_LEEWAY.seconds.from_now
end
refresh_token = snaptrade_item.oauth_refresh_token
raise AuthenticationError, "SnapTrade item has no refresh token" if refresh_token.blank?
payload = self.class.refresh_tokens(refresh_token: refresh_token)
snaptrade_item.apply_oauth_tokens!(payload)
end
rescue AuthenticationError => e
mark_requires_update!
DebugLogEntry.capture(
category: "provider_sync",
level: :error,
message: "SnapTrade token refresh failed: #{e.message}",
source: "Provider::Snaptrade",
provider_key: "snaptrade",
family: snaptrade_item.try(:family),
metadata: { snaptrade_item_id: snaptrade_item.try(:id) }
)
raise
end
View on GitHub (pinned to e69894adb9)
Solutions
- Have the user re-authorize the item (new authorize_url flow) so a complete token set is stored
- Check the stored payload: in console inspect the item's oauth_refresh_token / the original exchange response for a refresh_token field before re-auth
- If re-auth also yields no refresh token, verify the OAuth app's allowed scopes/grants on dashboard.snaptrade.com
Example fix
# before
def perform(item_id)
Provider::Snaptrade.new(SnaptradeItem.find(item_id)).get_positions
end
# after
def perform(item_id)
item = SnaptradeItem.find(item_id)
if item.oauth_token_expires_at.present? && item.oauth_refresh_token.blank?
item.update!(status: :requires_update)
return
end
Provider::Snaptrade.new(item).get_positions
rescue Provider::Snaptrade::AuthenticationError
retry if item.reload.status_previously_changed?
end Defensive patterns
Strategy: try-catch
Validate before calling
# Skip proactive refresh when there is nothing to refresh with if item.oauth_token_expires_at.present? && item.oauth_refresh_token.blank? item.update!(status: :requires_update) end
Type guard
def snaptrade_item_refreshable?(item) item.oauth_access_token.present? && item.oauth_refresh_token.present? end
Try / catch
begin provider.get_positions rescue Provider::Snaptrade::AuthenticationError item.reload raise unless item.status_requires_update? # already flagged by the provider; surface re-auth to user end
Prevention
- After exchange_code, assert the payload contained refresh_token before persisting the item as fully connected
- Monitor items whose expiry is near but refresh token is blank and proactively ask users to reconnect
When it happens
Trigger: Item has an access token but the stored refresh token is nil -- e.g. the original token payload from SnapTrade contained no refresh_token, the column was cleared, or a previous partial apply_oauth_tokens! persisted only the access token. It fires on the first sync after the access token enters the 60-second leeway window or gets a 401.
Common situations: SnapTrade issued no offline refresh token for the authorization (scope/grant configuration); encrypted-attribute migration or restore losing the refresh column; concurrent flows where an older row version is written back; items connected against a pre-release API that changed its token payload shape.
Related errors
- SnapTrade OAuth token request failed: #{error}
- SnapTrade item has no access token
- Authentication failed (#{operation}): HTTP #{response.status
- pagination_error
- request_failed
AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21).
Data as JSON: /api/errors/b21422e3ec3d5084.
Report an issue: GitHub.