we-promise/sure · error · Provider::Snaptrade::ApiError
SnapTrade OAuth token request failed: #{error}
Error message
SnapTrade OAuth token request failed: #{error} What it means
Raised by Provider::Snaptrade's OAuth token client when the SnapTrade token endpoint answers 4xx during a token request (exchange or refresh). The message combines the static prefix with the endpoint's own error_description or error field (falling back to 'HTTP <status>'). 4xx on a token endpoint is the OAuth server explicitly rejecting the request - bad credentials, bad code, or bad redirect_uri.
Source
Thrown at app/models/provider/snaptrade.rb:131
# processed it, replaying the same params would fail with invalid_grant
# even though the original request actually succeeded.
response = without_retry("POST #{TOKEN_URL}") do
oauth_connection.post(TOKEN_URL) do |request|
request.headers["Authorization"] = basic_auth_header
request.headers["Content-Type"] = "application/x-www-form-urlencoded"
request.body = URI.encode_www_form(params)
end
end
payload = parse_json(response.body)
return payload if response.success?
error = payload["error_description"].presence || payload["error"].presence || "HTTP #{response.status}"
if (400..499).cover?(response.status)
raise AuthenticationError, "SnapTrade OAuth token request failed: #{error}"
end
raise ApiError.new(
"SnapTrade OAuth token request failed: #{error}",
status_code: response.status, response_body: response.body
)
end
def basic_auth_header
"Basic #{Base64.strict_encode64("#{oauth_client_id}:#{oauth_client_secret}")}"
end
def oauth_connection
Faraday.new do |faraday|
faraday.options.timeout = 30
faraday.options.open_timeout = 10
end
end
def parse_json(body)
body.present? ? JSON.parse(body) : {}View on GitHub (pinned to e69894adb9)
Solutions
- Verify SNAPTRADE_CLIENT_ID and SNAPTRADE_CLIENT_SECRET against the SnapTrade dashboard - invalid_client is the most common cause
- Confirm the authorization code is fresh and used exactly once - never retry the token exchange with the same code after any failure
- Compare the redirect_uri sent in the token request byte-for-byte with the one used in the authorize step (scheme, host, path, no trailing-slash drift)
- Log/inspect the parsed error_description (already extracted into the message) - OAuth servers name the exact invalid parameter
Example fix
# before - code retried on failure (single-use codes get consumed) result = exchange_code(code) rescue retry # after - fail fast, never replay a single-use code result = exchange_code(code) # on AuthenticationError, restart the OAuth flow for a new code
Defensive patterns
Strategy: validation
Validate before calling
raise ArgumentError, "SNAPTRADE_CLIENT_ID/SECRET missing" if ENV["SNAPTRADE_CLIENT_ID"].blank? || ENV["SNAPTRADE_CLIENT_SECRET"].blank? raise ArgumentError, "code must be present and single-use" if code.blank? || code_used?(code) mark_code_used!(code) # reserve before the single exchange attempt
Try / catch
begin tokens = oauth_client.exchange_code(code) rescue Provider::Snaptrade::AuthenticationError => e restart_authorization_flow # code may be consumed; never replay it end
Prevention
- Never retry a token exchange with the same authorization code
- Assert client credentials are present at boot, not at first OAuth attempt
- Keep redirect_uri in one shared constant used by both authorize and token steps
When it happens
Trigger: POST to the SnapTrade OAuth token endpoint returns 400/401: invalid_client (wrong oauth_client_id/oauth_client_secret), invalid_grant (authorization code expired, already redeemed once, or redirect_uri mismatch), invalid_request (missing parameter in the encoded form body).
Common situations: SNAPTRADE_CLIENT_ID/SECRET env vars wrong or rotated after regenerating keys in the SnapTrade dashboard; the authorization code was consumed by an earlier attempt (codes are single-use) and is being reused on retry; redirect_uri differs by trailing slash or scheme between the authorize request and token exchange; clock skew making a freshly issued code look expired.
Related errors
- SnapTrade item has no access token
- SnapTrade item has no refresh token
- pagination_error
- request_failed
- Network error after #{max_retries} retries: #{e.message}
AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21).
Data as JSON: /api/errors/446c5880b03ecb42.
Report an issue: GitHub.