{"record":{"id":"700410061a331982","repo":"we-promise/sure","slug":"fetch-failed-700410","errorCode":"fetch_failed","errorMessage":"Sophtron API request failed: #{response.code} #{response.message} - #{body}","messagePattern":"Sophtron API request failed: #(.+?) #(.+?) - #(.+?)","errorType":"exception","errorClass":"Provider::Sophtron::Error","httpStatus":null,"severity":"error","filePath":"app/models/provider/sophtron.rb","lineNumber":367,"sourceCode":"      body = response.body.to_s\n\n      case response.code.to_i\n      when 200, 201, 204\n        return {} if body.strip.blank?\n\n        parse_json ? JSON.parse(body, symbolize_names: true) : parse_optional_json(body)\n      when 400\n        raise Error.new(\"Bad request to Sophtron API: #{body}\", :bad_request, details: body)\n      when 401\n        raise Error.new(\"Invalid Sophtron User ID or Access Key\", :unauthorized, details: body)\n      when 403\n        raise Error.new(\"Access forbidden by Sophtron\", :access_forbidden, details: body)\n      when 404\n        raise Error.new(\"Sophtron resource not found\", :not_found, details: body)\n      when 429\n        raise Error.new(\"Sophtron rate limit exceeded. Please try again later.\", :rate_limited, details: body)\n      else\n        raise Error.new(\n          \"Sophtron API request failed: #{response.code} #{response.message} - #{body}\",\n          :fetch_failed,\n          details: body\n        )\n      end\n    rescue JSON::ParserError => e\n      raise Error.new(\"Invalid JSON response from Sophtron API: #{e.message}\", :invalid_response, details: body)\n    end\n\n    def parse_optional_json(body)\n      JSON.parse(body, symbolize_names: true)\n    rescue JSON::ParserError\n      body\n    end\n\n    def normalize_base_url(value)\n      url = value.presence || DEFAULT_BASE_URL\n      url = url.to_s.chomp(\"/\")","sourceCodeStart":349,"sourceCodeEnd":385,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/sophtron.rb#L349-L385","documentation":"Provider::Sophtron's catch-all branch: any HTTP status not explicitly mapped (i.e. anything besides 200/201/204/400/401/403/404/429 - typically 5xx and unexpected 3xx/4xx) raises Error with error_type=:fetch_failed. The message interpolates response.code, response.message and the body, so the exact upstream failure is visible.","triggerScenarios":"Sophtron API 500/502/503/504 during bank aggregation (upstream bank outages surface as 5xx from Sophtron); gateway timeouts on long-running transaction history fetches; unexpected 422/409 responses from V1 RPC endpoints.","commonSituations":"A Sophtron or upstream-bank incident; requesting very deep transaction history (MAX_TRANSACTION_HISTORY_YEARS) that times out at the gateway; temporary Cloudflare/proxy 502s between the app and api.sophtron.com.","solutions":["Retry with backoff - 5xx from an aggregator is usually transient","Check status.sophtron.com or Sophtron support for an ongoing incident before debugging code","Log response.code/response.message from the error message to distinguish 5xx (transient) from unmapped 4xx (contract change)","If a new 4xx appears consistently (e.g. 422), map it to its own error_type branch instead of leaving it in the catch-all"],"exampleFix":"# before\nSophtronItem::Importer.new(item, sophtron_provider: p, sync: sync).import\n\n# after - retry transient 5xx\nbegin\n  SophtronItem::Importer.new(item, sophtron_provider: p, sync: sync).import\nrescue Provider::Sophtron::Error => e\n  retry if e.error_type == :fetch_failed && e.message.match?(/\\b5\\d\\d\\b/) && (retries += 1) < 3 && sleep(2 ** retries)\n  raise\nend","handlingStrategy":"retry","validationCode":"null","typeGuard":"def sophtron_fetch_failed_5xx?(err)\n  err.is_a?(Provider::Sophtron::Error) && err.error_type == :fetch_failed && err.message.match?(/\\b5\\d\\d\\b/)\nend","tryCatchPattern":"retries = 0\nbegin\n  importer.import\nrescue Provider::Sophtron::Error => e\n  retry if e.error_type == :fetch_failed && (retries += 1) <= 2 && sleep(2**retries * 10)\n  raise # 4xx-shaped fetch_failed means contract change - investigate, don't loop\nend","preventionTips":["Distinguish 5xx from unmapped 4xx in the message before retrying","Alert when the same unmapped status recurs - it signals an API contract change needing a new case branch","Keep Sophtron status/incident feed monitored during heavy sync windows"],"tags":["sophtron","http-5xx","upstream-failure","retry","catch-all"],"backgroundTag":"upstream-server-error","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}