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)
        end

View on GitHub (pinned to e69894adb9)

Solutions

  1. Inspect err.response_body - the first bytes usually identify an HTML page or truncated JSON
  2. Reproduce the exact request with curl and check Content-Type of the response
  3. If a proxy/WAF is in the path, bypass or configure it to pass SnapTrade traffic untouched
  4. 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

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

Related errors


AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21). Data as JSON: /api/errors/92de8dc11e735466. Report an issue: GitHub.