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.Wrap JSON parsing with response validation:
- 2.```ruby
- 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.Use safe JSON parsing with fallback:
- 2.```ruby
- 3.class SafeJsonParser
- 4.def self.parse(body, fallback = nil)
- 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.Handle HTTP error status codes before parsing:
- 2.```ruby
- 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.Handle encoding issues in JSON response:
- 2.```ruby
- 3.def parse_with_encoding_fix(body)
- 4.# Remove BOM and fix common encoding issues
- 5.clean_body = body
- 6..delete("\uFEFF") # Remove BOM
- 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-Typeheader 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_schemergem - Set up monitoring for JSON parse error rates and alert on spikes