we-promise/sure · error · Provider::Snaptrade::AuthenticationError
SnapTrade item has no access token
Error message
SnapTrade item has no access token
What it means
Provider::Snaptrade.new(snaptrade_item) makes API calls via request_json, whose first step is ensure_fresh_token!. It raises AuthenticationError when the item row has no oauth_access_token at all -- i.e. the connection record exists but the OAuth code exchange was never completed or the stored token was cleared. Unlike refresh_access_token!, this raise is not wrapped in the mark_requires_update!/DebugLogEntry rescue, so it propagates straight to the caller.
Source
Thrown at app/models/provider/snaptrade.rb:314
request.headers["Accept"] = "application/json"
request.params.update(params) if params.present?
if body
request.headers["Content-Type"] = "application/json"
request.body = body.to_json
end
end
end
if response.status == 401 && retry_on_auth_failure
refresh_access_token!(previous_access_token: used_access_token)
return request_json(method, path, params: params, body: body, retry_on_auth_failure: false)
end
handle_response(response, operation)
end
def ensure_fresh_token!
raise AuthenticationError, "SnapTrade item has no access token" if snaptrade_item.oauth_access_token.blank?
expires_at = snaptrade_item.oauth_token_expires_at
return if expires_at.blank? || expires_at > TOKEN_EXPIRY_LEEWAY.seconds.from_now
refresh_access_token!
end
# Guards against a concurrent refresh-token rotation race: multiple threads/processes
# (e.g. per-account jobs sharing one SnapTrade item) may all observe an expiring/rejected
# token and attempt to refresh at once. If SnapTrade rotates refresh tokens as single-use,
# every refresh after the first would fail with invalid_grant and needlessly brick the
# item. Taking a DB row lock and re-checking freshness after reload ensures only one
# caller actually performs the HTTP refresh; the rest observe the winner's fresh token.
#
# `previous_access_token`, when present, means we're refreshing reactively after a 401 on
# 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 beView on GitHub (pinned to e69894adb9)
Solutions
- Route the user back through the OAuth authorize flow so exchange_code stores a token (item gets apply_oauth_tokens!)
- Before syncing, check snaptrade_item.oauth_access_token.present? and skip/flag the item instead of instantiating API calls
- Rescue Provider::Snaptrade::AuthenticationError in the sync entry point and mark the item status :requires_update
Example fix
# before
SyncJob.perform_later(item.id)
# SyncJob:
positions = Provider::Snaptrade.new(item).get_positions
# after
# SyncJob:
def perform(item_id)
item = SnaptradeItem.find(item_id)
unless item.oauth_access_token.present?
item.update!(status: :requires_update)
return
end
positions = Provider::Snaptrade.new(item).get_positions
end Defensive patterns
Strategy: validation
Validate before calling
# Before any API work return if snaptrade_item.oauth_access_token.blank?
Type guard
def snaptrade_item_authorized?(item) item.oauth_access_token.present? end
Try / catch
begin provider.get_positions rescue Provider::Snaptrade::AuthenticationError => e item.update!(status: :requires_update) # prompt re-authorization end
Prevention
- Only create the SnaptradeItem row after exchange_code succeeded and tokens were persisted
- Filter sync jobs on oauth_access_token.present? at enqueue time
When it happens
Trigger: A sync job or holdings fetch runs against a SnaptradeItem that was created (e.g. placeholder row from an import or an aborted OAuth flow) but never received apply_oauth_tokens! after exchange_code. Also any code path that constructs the provider and calls get_positions/get_connection_url/delete_connection before authorization completed.
Common situations: User abandons the OAuth flow mid-callback (callback errored once, row persisted); test fixtures creating snaptrade_items without tokens; a data migration or manual console edit blanking oauth_access_token.
Related errors
- pagination_error
- SnapTrade OAuth token request failed: #{error}
- SnapTrade item has no refresh token
- This property data provider is not configured.
- {response.error.message}
AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21).
Data as JSON: /api/errors/b5d27d904be22627.
Report an issue: GitHub.