{"record":{"id":"446c5880b03ecb42","repo":"we-promise/sure","slug":"snaptrade-oauth-token-request-failed-error","errorCode":null,"errorMessage":"SnapTrade OAuth token request failed: #{error}","messagePattern":"SnapTrade OAuth token request failed: #(.+?)","errorType":"exception","errorClass":"Provider::Snaptrade::ApiError","httpStatus":null,"severity":"error","filePath":"app/models/provider/snaptrade.rb","lineNumber":131,"sourceCode":"        # processed it, replaying the same params would fail with invalid_grant\n        # even though the original request actually succeeded.\n        response = without_retry(\"POST #{TOKEN_URL}\") do\n          oauth_connection.post(TOKEN_URL) do |request|\n            request.headers[\"Authorization\"] = basic_auth_header\n            request.headers[\"Content-Type\"] = \"application/x-www-form-urlencoded\"\n            request.body = URI.encode_www_form(params)\n          end\n        end\n\n        payload = parse_json(response.body)\n        return payload if response.success?\n\n        error = payload[\"error_description\"].presence || payload[\"error\"].presence || \"HTTP #{response.status}\"\n        if (400..499).cover?(response.status)\n          raise AuthenticationError, \"SnapTrade OAuth token request failed: #{error}\"\n        end\n\n        raise ApiError.new(\n          \"SnapTrade OAuth token request failed: #{error}\",\n          status_code: response.status, response_body: response.body\n        )\n      end\n\n      def basic_auth_header\n        \"Basic #{Base64.strict_encode64(\"#{oauth_client_id}:#{oauth_client_secret}\")}\"\n      end\n\n      def oauth_connection\n        Faraday.new do |faraday|\n          faraday.options.timeout = 30\n          faraday.options.open_timeout = 10\n        end\n      end\n\n      def parse_json(body)\n        body.present? ? JSON.parse(body) : {}","sourceCodeStart":113,"sourceCodeEnd":149,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/snaptrade.rb#L113-L149","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":"# before - code retried on failure (single-use codes get consumed)\nresult = exchange_code(code) rescue retry\n\n# after - fail fast, never replay a single-use code\nresult = exchange_code(code) # on AuthenticationError, restart the OAuth flow for a new code","handlingStrategy":"validation","validationCode":"raise ArgumentError, \"SNAPTRADE_CLIENT_ID/SECRET missing\" if ENV[\"SNAPTRADE_CLIENT_ID\"].blank? || ENV[\"SNAPTRADE_CLIENT_SECRET\"].blank?\nraise ArgumentError, \"code must be present and single-use\" if code.blank? || code_used?(code)\nmark_code_used!(code) # reserve before the single exchange attempt","typeGuard":null,"tryCatchPattern":"begin\n  tokens = oauth_client.exchange_code(code)\nrescue Provider::Snaptrade::AuthenticationError => e\n  restart_authorization_flow # code may be consumed; never replay it\nend","preventionTips":["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"],"tags":["snaptrade","oauth","token-exchange","authentication","invalid-grant"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}