we-promise/sure · error · Provider::Snaptrade::ApiError
SnapTrade API error (#{operation}): invalid JSON response
Error message
SnapTrade API error (#{operation}): invalid JSON response What it means
Raised by Provider::Snaptrade.handle_response when a successful (2xx) response body fails JSON.parse. The ApiError carries status_code and response_body so the offending payload can be inspected. A 2xx with a non-JSON body almost always means the bytes came from something other than the JSON API - an HTML error page, a truncated body, or a proxy interjection.
Source
Thrown at app/models/provider/snaptrade.rb:379
family: snaptrade_item.try(:family),
metadata: { snaptrade_item_id: snaptrade_item.try(:id) }
)
raise
end
def mark_requires_update!
snaptrade_item.update!(status: :requires_update)
rescue StandardError => e
Rails.logger.warn("SnapTrade: could not mark item requires_update: #{e.message}")
end
def handle_response(response, operation)
if response.success?
return {} if response.body.blank?
begin
JSON.parse(response.body)
rescue JSON::ParserError
raise ApiError.new("SnapTrade API error (#{operation}): invalid JSON response",
status_code: response.status, response_body: response.body)
end
else
Rails.logger.error("SnapTrade API error (#{operation}): #{response.status}")
case response.status
when 401, 403
mark_requires_update!
raise AuthenticationError, "Authentication failed (#{operation}): HTTP #{response.status}"
when 429
raise ApiError.new("Rate limit exceeded. Please try again later.",
status_code: response.status, response_body: response.body)
when 500..599
raise ApiError.new("SnapTrade server error (#{response.status}). Please try again later.",
status_code: response.status, response_body: response.body)
else
raise ApiError.new("SnapTrade API error (#{operation}): HTTP #{response.status}",
status_code: response.status, response_body: response.body)
endView on GitHub (pinned to e69894adb9)
Solutions
- Inspect err.response_body - the first bytes usually identify an HTML page or truncated JSON
- Reproduce the exact request with curl and check Content-Type of the response
- If a proxy/WAF is in the path, bypass or configure it to pass SnapTrade traffic untouched
- Treat as transient first (retry once) - truncation and interstitials are usually momentary; persistent failure points to middleware
Defensive patterns
Strategy: try-catch
Type guard
def json_api_body?(body)
body.present? && body.strip.start_with?("{", "[")
end Try / catch
begin
data = snaptrade.get_balances(account_id: id)
rescue Provider::Snaptrade::ApiError => e
if e.message.include?("invalid JSON")
log_response_body_sample(e.response_body) # identify HTML page / truncation
retry_once_later
else
raise
end
end Prevention
- Record e.response_body (truncated) when this fires - it identifies proxy/HTML interjection
- Ensure no HTML-serving middleware sits between the app and api.snaptrade.com
- Treat 2xx-with-bad-JSON as a distinct alert class from HTTP errors
When it happens
Trigger: Any SnapTrade API call whose 2xx body is HTML (CDN/WAF error page served with 200), a body truncated mid-transfer by a dropped connection, a BOM or leading whitespace causing a parse failure, or a maintenance/HTML splash page returned while the JSON backend is down.
Common situations: Corporate proxy or Cloudflare serving an interstitial page; SnapTrade behind a load balancer that returns 200 with an HTML 'temporarily unavailable' page; intermittent truncation on slow mobile networks; response stored via a caching layer that mutated the body.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- Network error after #{max_retries} retries: #{e.message}
- SnapTrade API error (#{operation}): HTTP #{response.status}
- Could not save that passkey or security key. Please try agai
- Lunchflow provider is not configured
- pagination_error
AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21).
Data as JSON: /api/errors/92de8dc11e735466.
Report an issue: GitHub.