{"record":{"id":"e0780c02b6c94018","repo":"we-promise/sure","slug":"authentication-failed-operation-http-respo","errorCode":null,"errorMessage":"Authentication failed (#{operation}): HTTP #{response.status}","messagePattern":"Authentication failed \\(#(.+?)\\): HTTP #(.+?)","errorType":"exception","errorClass":"Provider::Snaptrade::AuthenticationError","httpStatus":null,"severity":"error","filePath":"app/models/provider/snaptrade.rb","lineNumber":387,"sourceCode":"    rescue StandardError => e\n      Rails.logger.warn(\"SnapTrade: could not mark item requires_update: #{e.message}\")\n    end\n\n    def handle_response(response, operation)\n      if response.success?\n        return {} if response.body.blank?\n        begin\n          JSON.parse(response.body)\n        rescue JSON::ParserError\n          raise ApiError.new(\"SnapTrade API error (#{operation}): invalid JSON response\",\n                             status_code: response.status, response_body: response.body)\n        end\n      else\n        Rails.logger.error(\"SnapTrade API error (#{operation}): #{response.status}\")\n        case response.status\n        when 401, 403\n          mark_requires_update!\n          raise AuthenticationError, \"Authentication failed (#{operation}): HTTP #{response.status}\"\n        when 429\n          raise ApiError.new(\"Rate limit exceeded. Please try again later.\",\n                             status_code: response.status, response_body: response.body)\n        when 500..599\n          raise ApiError.new(\"SnapTrade server error (#{response.status}). Please try again later.\",\n                             status_code: response.status, response_body: response.body)\n        else\n          raise ApiError.new(\"SnapTrade API error (#{operation}): HTTP #{response.status}\",\n                             status_code: response.status, response_body: response.body)\n        end\n      end\n    end\n\n    def api_connection\n      @api_connection ||= Faraday.new do |faraday|\n        faraday.options.timeout = 30\n        faraday.options.open_timeout = 10\n      end","sourceCodeStart":369,"sourceCodeEnd":405,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/snaptrade.rb#L369-L405","documentation":"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.","triggerScenarios":"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'.","commonSituations":"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.","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"],"exampleFix":"# before\nbegin\n  provider.get_positions\nrescue => e\n  Rails.logger.error(e.message)\nend\n\n# after\nbegin\n  provider.get_positions\nrescue Provider::Snaptrade::AuthenticationError => e\n  item.update!(status: :requires_update)\n  NotifyUserReauthorizationJob.perform_later(item.id)\nend","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"begin\n  provider.get_positions\nrescue Provider::Snaptrade::AuthenticationError => e\n  # provider already set item.status = requires_update; never retry with the same token\n  ReauthorizationPromptJob.perform_later(item.id)\nend","preventionTips":["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"],"tags":["snaptrade","http-401","http-403","authentication","requires-update"],"backgroundTag":"http-401-unauthorized","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}