we-promise/sure · error · Provider::Snaptrade::AuthenticationError
Authentication failed (#{operation}): HTTP #{response.status
Error message
Authentication failed (#{operation}): HTTP #{response.status} What it means
handle_response is the shared non-2xx handler for all SnapTrade data API calls. A 401 or 403 marks the item status :requires_update and raises AuthenticationError with the operation name and HTTP status. 401 means the Bearer access token was rejected (expired and refresh failed, revoked at SnapTrade, or never valid); 403 means the token is valid but the app/token lacks permission for that endpoint. Note request_json already tried one automatic refresh+replay on 401 before this can raise, so reaching here means the token is durably unusable.
Source
Thrown at app/models/provider/snaptrade.rb:387
rescue StandardError => e
Rails.logger.warn("SnapTrade: could not mark item requires_update: #{e.message}")
end
def handle_response(response, operation)
if response.success?
return {} if response.body.blank?
begin
JSON.parse(response.body)
rescue JSON::ParserError
raise ApiError.new("SnapTrade API error (#{operation}): invalid JSON response",
status_code: response.status, response_body: response.body)
end
else
Rails.logger.error("SnapTrade API error (#{operation}): #{response.status}")
case response.status
when 401, 403
mark_requires_update!
raise AuthenticationError, "Authentication failed (#{operation}): HTTP #{response.status}"
when 429
raise ApiError.new("Rate limit exceeded. Please try again later.",
status_code: response.status, response_body: response.body)
when 500..599
raise ApiError.new("SnapTrade server error (#{response.status}). Please try again later.",
status_code: response.status, response_body: response.body)
else
raise ApiError.new("SnapTrade API error (#{operation}): HTTP #{response.status}",
status_code: response.status, response_body: response.body)
end
end
end
def api_connection
@api_connection ||= Faraday.new do |faraday|
faraday.options.timeout = 30
faraday.options.open_timeout = 10
endView on GitHub (pinned to e69894adb9)
Solutions
- Check the item: status is now requires_update -- send the user through the OAuth authorize flow again
- If 403: review the OAuth app's granted scopes on dashboard.snaptrade.com and the scope requested in authorize_url (default 'read')
- If 401 persists immediately after re-auth, confirm server clock accuracy and that SNAPTRADE_OAUTH credentials match the dashboard app
Example fix
# before begin provider.get_positions rescue => e Rails.logger.error(e.message) end # after begin provider.get_positions rescue Provider::Snaptrade::AuthenticationError => e item.update!(status: :requires_update) NotifyUserReauthorizationJob.perform_later(item.id) end
Defensive patterns
Strategy: try-catch
Try / catch
begin provider.get_positions rescue Provider::Snaptrade::AuthenticationError => e # provider already set item.status = requires_update; never retry with the same token ReauthorizationPromptJob.perform_later(item.id) end
Prevention
- Check item.status == :requires_update before dispatching work and route to re-auth immediately
- Don't hand-roll retries around 401s: request_json already refreshes once; a second failure means re-authorization
When it happens
Trigger: Any get_positions/get_connection_url/delete_connection call where SnapTrade returns 401 after the built-in refresh retry was exhausted (refresh failed with invalid_grant), or 403 for an endpoint outside the OAuth app's granted scope. The message names the failing operation, e.g. 'Authentication failed (GET /api/v1/positions): HTTP 401'.
Common situations: User disconnected/relinked the account at SnapTrade dashboard (old token revoked); token revoked via revoke_token elsewhere; app permissions changed on the SnapTrade side; long-lived background jobs using a token that was rotated by a newer session.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- SnapTrade item has no refresh token
- Trading 212 authentication failed (#{response.code}). Check
- unauthorized
- unauthorized
- unauthorized
AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21).
Data as JSON: /api/errors/e0780c02b6c94018.
Report an issue: GitHub.