Introduction

JSON::ParserError is a frequent production error when your Ruby application parses responses from external APIs. The error occurs because the API returned something that is not valid JSON: an HTML error page (502/503), an empty body, plain text error message, or malformed JSON with encoding issues.

Symptoms

  • JSON::ParserError: 783: unexpected token at '<html><head>'
  • JSON::ParserError: 783: unexpected token at '' (empty response)
  • JSON::ParserError: 416: unexpected token at '{"valid": json,}' (malformed JSON)
  • Error occurs intermittently when the external API is under load
  • API returns HTTP 200 with an HTML maintenance page

Example error: `` JSON::ParserError: 783: unexpected token at '<!DOCTYPE html> <html><head><title>502 Bad Gateway</title></head> <body><h1>Bad Gateway</h1></body></html>' from /path/to/json-2.7.1/lib/json/common.rb:222:in parse' from /path/to/json-2.7.1/lib/json/common.rb:222:in parse' from app/services/payment_gateway.rb:45:in process_payment' ```

Common Causes

  • External API returns HTML error page (502, 503, 504)
  • API returns empty response body on timeout
  • API changes response format without version notice
  • JSON response contains invalid encoding (non-UTF8 characters)
  • Rate limiter returns plain text response instead of JSON

Step-by-Step Fix

  1. 1.Wrap JSON parsing with response validation:
  2. 2.```ruby
  3. 3.require 'json'

class ApiClient def parse_json_response(response) content_type = response.headers['content-type'] || ''

unless content_type.include?('application/json') raise UnexpectedResponseError, "Expected JSON, got #{content_type}. Body: #{response.body[0..200]}" end

JSON.parse(response.body) rescue JSON::ParserError => e raise JsonParseError, "Failed to parse JSON response (HTTP #{response.code}): #{e.message}. " \ "Body preview: #{response.body.to_s[0..500].inspect}" end end ```

  1. 1.Use safe JSON parsing with fallback:
  2. 2.```ruby
  3. 3.class SafeJsonParser
  4. 4.def self.parse(body, fallback = nil)
  5. 5.return fallback if body.nil? || body.empty?

JSON.parse(body) rescue JSON::ParserError, TypeError Rails.logger.error "JSON parse failed for body: #{body.to_s[0..200].inspect}" fallback end end

# Usage data = SafeJsonParser.parse(response.body, { error: true, raw: response.body }) ```

  1. 1.Handle HTTP error status codes before parsing:
  2. 2.```ruby
  3. 3.response = HTTParty.get('https://api.example.com/data')

case response.code when 200 data = JSON.parse(response.body) process(data) when 429 sleep(response.headers['Retry-After'].to_i || 60) retry when 500..599 raise ExternalServiceError, "API returned #{response.code}: #{response.body[0..100]}" else raise UnexpectedHttpResponseError, "HTTP #{response.code}: #{response.body[0..200]}" end ```

  1. 1.Handle encoding issues in JSON response:
  2. 2.```ruby
  3. 3.def parse_with_encoding_fix(body)
  4. 4.# Remove BOM and fix common encoding issues
  5. 5.clean_body = body
  6. 6..delete("\uFEFF") # Remove BOM
  7. 7..encode('UTF-8', invalid: :replace, undef: :replace, replace: '')

JSON.parse(clean_body) rescue JSON::ParserError => e # Try with relaxed parsing JSON.parse(clean_body, quirks_mode: true) end ```

Prevention

  • Always check HTTP status code before parsing JSON
  • Validate Content-Type header matches expected format
  • Log the first 500 characters of unexpected responses for debugging
  • Use circuit breakers to stop calling failing APIs
  • Add response schema validation with json_schemer gem
  • Set up monitoring for JSON parse error rates and alert on spikes