we-promise/sure · error · Provider::Sophtron::Error
fetch_failed
fetch_failed
Error message
Sophtron API request failed: #{response.code} #{response.message} - #{body} What it means
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.
Source
Thrown at app/models/provider/sophtron.rb:367
body = response.body.to_s
case response.code.to_i
when 200, 201, 204
return {} if body.strip.blank?
parse_json ? JSON.parse(body, symbolize_names: true) : parse_optional_json(body)
when 400
raise Error.new("Bad request to Sophtron API: #{body}", :bad_request, details: body)
when 401
raise Error.new("Invalid Sophtron User ID or Access Key", :unauthorized, details: body)
when 403
raise Error.new("Access forbidden by Sophtron", :access_forbidden, details: body)
when 404
raise Error.new("Sophtron resource not found", :not_found, details: body)
when 429
raise Error.new("Sophtron rate limit exceeded. Please try again later.", :rate_limited, details: body)
else
raise Error.new(
"Sophtron API request failed: #{response.code} #{response.message} - #{body}",
:fetch_failed,
details: body
)
end
rescue JSON::ParserError => e
raise Error.new("Invalid JSON response from Sophtron API: #{e.message}", :invalid_response, details: body)
end
def parse_optional_json(body)
JSON.parse(body, symbolize_names: true)
rescue JSON::ParserError
body
end
def normalize_base_url(value)
url = value.presence || DEFAULT_BASE_URL
url = url.to_s.chomp("/")View on GitHub (pinned to e69894adb9)
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
Example fix
# before SophtronItem::Importer.new(item, sophtron_provider: p, sync: sync).import # after - retry transient 5xx begin SophtronItem::Importer.new(item, sophtron_provider: p, sync: sync).import rescue Provider::Sophtron::Error => e retry if e.error_type == :fetch_failed && e.message.match?(/\b5\d\d\b/) && (retries += 1) < 3 && sleep(2 ** retries) raise end
Defensive patterns
Strategy: retry
Validate before calling
null
Type guard
def sophtron_fetch_failed_5xx?(err) err.is_a?(Provider::Sophtron::Error) && err.error_type == :fetch_failed && err.message.match?(/\b5\d\d\b/) end
Try / catch
retries = 0 begin importer.import rescue Provider::Sophtron::Error => e retry if e.error_type == :fetch_failed && (retries += 1) <= 2 && sleep(2**retries * 10) raise # 4xx-shaped fetch_failed means contract change - investigate, don't loop end
Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21).
Data as JSON: /api/errors/700410061a331982.
Report an issue: GitHub.