{"record":{"id":"92de8dc11e735466","repo":"we-promise/sure","slug":"snaptrade-api-error-operation-invalid-json-r","errorCode":null,"errorMessage":"SnapTrade API error (#{operation}): invalid JSON response","messagePattern":"SnapTrade API error \\(#(.+?)\\): invalid JSON response","errorType":"exception","errorClass":"Provider::Snaptrade::ApiError","httpStatus":null,"severity":"error","filePath":"app/models/provider/snaptrade.rb","lineNumber":379,"sourceCode":"        family: snaptrade_item.try(:family),\n        metadata: { snaptrade_item_id: snaptrade_item.try(:id) }\n      )\n      raise\n    end\n\n    def mark_requires_update!\n      snaptrade_item.update!(status: :requires_update)\n    rescue StandardError => e\n      Rails.logger.warn(\"SnapTrade: could not mark item requires_update: #{e.message}\")\n    end\n\n    def handle_response(response, operation)\n      if response.success?\n        return {} if response.body.blank?\n        begin\n          JSON.parse(response.body)\n        rescue JSON::ParserError\n          raise ApiError.new(\"SnapTrade API error (#{operation}): invalid JSON response\",\n                             status_code: response.status, response_body: response.body)\n        end\n      else\n        Rails.logger.error(\"SnapTrade API error (#{operation}): #{response.status}\")\n        case response.status\n        when 401, 403\n          mark_requires_update!\n          raise AuthenticationError, \"Authentication failed (#{operation}): HTTP #{response.status}\"\n        when 429\n          raise ApiError.new(\"Rate limit exceeded. Please try again later.\",\n                             status_code: response.status, response_body: response.body)\n        when 500..599\n          raise ApiError.new(\"SnapTrade server error (#{response.status}). Please try again later.\",\n                             status_code: response.status, response_body: response.body)\n        else\n          raise ApiError.new(\"SnapTrade API error (#{operation}): HTTP #{response.status}\",\n                             status_code: response.status, response_body: response.body)\n        end","sourceCodeStart":361,"sourceCodeEnd":397,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/snaptrade.rb#L361-L397","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":"def json_api_body?(body)\n  body.present? && body.strip.start_with?(\"{\", \"[\")\nend","tryCatchPattern":"begin\n  data = snaptrade.get_balances(account_id: id)\nrescue Provider::Snaptrade::ApiError => e\n  if e.message.include?(\"invalid JSON\")\n    log_response_body_sample(e.response_body) # identify HTML page / truncation\n    retry_once_later\n  else\n    raise\n  end\nend","preventionTips":["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"],"tags":["snaptrade","json-parse","html-response","proxy","faraday"],"backgroundTag":"invalid-json-response","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}