{"record":{"id":"a5d1223971c4b9e8","repo":"we-promise/sure","slug":"access-forbidden-a5d122","errorCode":"access_forbidden","errorMessage":"Access URL is no longer valid","messagePattern":"Access URL is no longer valid","errorType":"exception","errorClass":"Provider::Simplefin::SimplefinError","httpStatus":403,"severity":"critical","filePath":"app/models/provider/simplefin.rb","lineNumber":92,"sourceCode":"\n    accounts_url = \"#{access_url}/accounts\"\n    accounts_url += \"?#{URI.encode_www_form(query_params)}\" unless query_params.empty?\n\n    # The access URL already contains HTTP Basic Auth credentials\n    # Use retry logic with exponential backoff for transient network failures\n    # Use self.class.get to inherit class-level SSL and timeout defaults\n    response = with_retries(\"GET /accounts\") do\n      self.class.get(accounts_url)\n    end\n\n    case response.code\n    when 200\n      JSON.parse(response.body, symbolize_names: true)\n    when 400\n      Rails.logger.error \"SimpleFin API: Bad request - #{response.body}\"\n      raise SimplefinError.new(\"Bad request to SimpleFin API: #{response.body}\", :bad_request)\n    when 403\n      raise SimplefinError.new(\"Access URL is no longer valid\", :access_forbidden)\n    when 402\n      raise SimplefinError.new(\"Payment required to access this account\", :payment_required)\n    when 429\n      Rails.logger.warn \"SimpleFin API: Rate limited - #{response.body}\"\n      raise SimplefinError.new(\"SimpleFin rate limit exceeded. Please try again later.\", :rate_limited)\n    when 500..599\n      Rails.logger.error \"SimpleFin API: Server error - Code: #{response.code}, Body: #{response.body}\"\n      raise SimplefinError.new(\"SimpleFin server error (#{response.code}). Please try again later.\", :server_error)\n    else\n      Rails.logger.error \"SimpleFin API: Unexpected response - Code: #{response.code}, Body: #{response.body}\"\n      raise SimplefinError.new(\"Failed to fetch accounts: #{response.code} #{response.message} - #{response.body}\", :fetch_failed)\n    end\n  end\n\n  def get_info(base_url)\n    # Use self.class.get to inherit class-level SSL and timeout defaults\n    response = self.class.get(\"#{base_url}/info\")\n","sourceCodeStart":74,"sourceCodeEnd":110,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/simplefin.rb#L74-L110","documentation":"Raised by Provider::Simplefin#get_accounts when the accounts endpoint returns 403, with error_type :access_forbidden. The claimed access URL embeds HTTP Basic Auth credentials; a 403 means those credentials were revoked server-side. This is permanent for that URL — the user must issue a new setup token and re-claim.","triggerScenarios":"User clicked 'reset/revoke access' at their SimpleFin bridge; bridge rotated credentials; the access URL was claimed from a different account context and got invalidated.","commonSituations":"Bank re-link flows where the old access URL persists locally; user troubleshooting by resetting the SimpleFin connection; bridge migrations invalidating old URLs.","solutions":["Have the user generate a new setup token and run the claim flow again","Clear the stored access URL immediately so jobs stop hitting the revoked credentials","Mark the SimpleFin connection as needing re-link and notify the user","Distinguish this from a one-time claim failure: here the URL worked before and has since been revoked"],"exampleFix":"# before\ndef sync(account)\n  data = client.get_accounts(account.access_url)\nend\n\n# after\ndef sync(account)\n  data = client.get_accounts(account.access_url)\nrescue Provider::Simplefin::SimplefinError => e\n  raise unless e.error_type == :access_forbidden\n  account.update!(access_url: nil, needs_relink: true)\n  UserMailer.simplefin_relink(account.user).deliver_later\nend","handlingStrategy":"try-catch","validationCode":"def simplefin_url_alive?(client, base_url)\n  client.get_info(base_url)\n  true\nrescue Provider::Simplefin::SimplefinError\n  false\nend","typeGuard":"def simplefin_access_revoked?(error)\n  error.is_a?(Provider::Simplefin::SimplefinError) && error.error_type == :access_forbidden\nend","tryCatchPattern":"begin\n  client.get_accounts(account.access_url)\nrescue Provider::Simplefin::SimplefinError => e\n  raise unless e.error_type == :access_forbidden\n  account.update!(access_url: nil, status: \"needs_relink\")\n  UserMailer.simplefin_relink(account.user).deliver_later\nend","preventionTips":["Null out stored access URLs the moment a 403 is seen — retrying a revoked URL never works","Build the relink (new setup token -> claim) flow before launch; 403s are inevitable over time","Distinguish :access_forbidden (revoked) from :token_compromised (claim-time) in support copy"],"tags":["simplefin","http-403","revoked-credentials","access-url","relink"],"backgroundTag":"revoked-access-credentials","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}