Fixing "SyntaxError: Unexpected token <" — How to handle APIs that return HTML instead of JSON
Short summary: That dreaded "Unexpected token '<'" almost always means your JSON parser received an HTML document (an error page, a login redirect, or an HTML response). This guide explains why it happens, how to diagnose it fast, defensive parsing patterns (JavaScript and Python), retry and backoff strategies, monitoring, and prevention.
Why this error happens and how to diagnose it
At parse-time the JSON parser sees the character '<' at the start of the payload — the beginning of an HTML tag — and throws SyntaxError. The root cause is not the parser: it's the server returning HTML when you expected JSON. Common culprits include 404/500 error pages, authentication redirects (login HTML), middleware that renders HTML for certain Accept headers, or misconfigured proxies.
Diagnosing requires inspecting the raw HTTP response: status code, headers, and the response body. Check Content-Type — it should be application/json (commonly application/json; charset=utf-8). If Content-Type is text/html or missing, treat the body as suspect. Also reproduce the call with curl or a browser to see the HTML; client-side frameworks sometimes hide the actual response.
Tools are your friend: use curl -i to see headers and body, Postman to examine the response, or server logs to identify 5xx pages. Capture the exact response body and search for common strings (e.g., "Error", "Traceback", "", or login page HTML) to pinpoint whether the server returned an error page or redirect.
Quick defensive patterns: check headers, then parse
Never assume a response is JSON. Always validate the response status and Content-Type before calling JSON.parse or response.json(). Defensive checks prevent crashes and make error handling explicit.
Example behavior: if content-type indicates JSON, parse; if content-type is HTML or the status is not OK, capture the text, log it, and surface a useful error. When you must parse potentially malformed JSON, wrap parsing in try/catch and include the raw snippet in logs (truncate for PII/security).
Below are compact code patterns for common stacks. They show safe parsing, fallback inspection, and how to surface meaningful error messages instead of letting the runtime throw a raw SyntaxError.
// JavaScript (fetch)
async function fetchJson(url, opts = {}) {
const res = await fetch(url, opts);
const ct = res.headers.get('content-type') || '';
const body = await res.text(); // read as text to allow inspection
if (!res.ok) {
console.error('API error', res.status, body.slice(0,200));
throw new Error(`API error: ${res.status}`);
}
if (!ct.includes('application/json')) {
console.error('Unexpected content-type', ct, body.slice(0,200));
throw new Error('Expected JSON response but got ' + ct);
}
try {
return JSON.parse(body);
} catch (err) {
console.error('JSON parse failed:', err, body.slice(0,400));
throw err;
}
}
# Python (requests)
import requests, json
def fetch_json(url, **kw):
r = requests.get(url, **kw)
ct = r.headers.get('Content-Type','')
text = r.text
if not r.ok:
print('API error', r.status_code, text[:200])
raise RuntimeError(f'API error: {r.status_code}')
if 'application/json' not in ct.lower():
print('Unexpected Content-Type', ct, text[:200])
raise RuntimeError('Expected JSON response')
try:
return r.json()
except json.JSONDecodeError as e:
print('JSON decode error', e, text[:400])
raise
How to test and reproduce: quick checklist
Before changing code, confirm the server is the source of HTML. A reproducible test helps isolate whether the client is misconfigured or the server is broken.
- Use curl -i or Postman to inspect headers and raw body; look for HTML and server stacktraces.
- Check for auth redirects: when a cookie/session is missing, endpoints may redirect to login pages that return HTML.
- Compare Accept headers — some servers render HTML when the Accept header prefers text/html.
- Check server logs for 4xx/5xx at the request timestamp and review proxy error pages.
If the HTML contains a stacktrace or templated error page, fix the server-side issue. If it’s an auth redirect, adjust client auth flow or use the correct token/cookie. If it’s a proxy or CDN returning an HTML error, inspect the proxy logs and configuration.
Retry mechanism and backoff strategies
Transient server errors (502/503/504 and rate limiting 429) justify retries; permanent errors (400/401/403/404) do not. Always classify responses before retrying so you don't unintentionally hammer a failing server.
Implement exponential backoff with jitter to reduce thundering herd problems. Limit the number of retries, add circuit breakers for persistent failures, and include idempotency checks when retrying non-idempotent requests.
- Retry on 429, 502, 503, 504 and network timeouts. Use exponential backoff with random jitter (e.g., base * 2^attempt ± jitter).
- Respect Retry-After header when present.
- Use a circuit breaker to stop retries when failure rate exceeds a threshold and recover after a cooldown.
Sample JS pseudo-code: attempt request, if status is retryable sleep for backoff and retry up to max attempts. Always log attempts and include the response snippet on final failure for postmortem analysis.
Monitoring, logging, and alerts to catch the problem early
Parsing errors are runtime indicators you should monitor. Instrument metrics for parse failures (count), mismatched content-type (count), and status-code spikes. Correlate these metrics with release deploys and infra events to speed root-cause analysis.
Log a truncated copy of the response body for abnormal responses, but sanitize PII and tokens. Capture request id, trace id, URL, and timestamps to stitch together distributed traces. Use observability platforms (Sentry, Datadog, New Relic) to create an alert when parse errors exceed a small threshold.
Create alerting rules: e.g., "if JSON parse failures > 5% of requests in 5 minutes and > 50 requests then page the on-call." Combine alerts with automated runbooks that include quick checks: curl endpoint, check service logs, check upstream dependencies, and disable recent deployments if correlated.
Prevention and best practices
Preventative steps on both client and server reduce the chance of encountering HTML where JSON is expected. On the server, always set Content-Type: application/json for JSON responses and return minimal JSON error objects for API clients instead of HTML error pages.
On the client, centralize response handling in a small, well-tested helper (like fetchJson above) so all callers get consistent behavior. Add unit tests and integration tests that simulate HTML error responses to ensure your error paths work and don't crash the app.
Document API contracts (status codes, headers, error body schema) and enforce them with contract tests or middleware. Use feature flags and gradual rollouts to limit blast radius for changes that could break content-type assumptions.
Practical examples and linkable resources
When you need to inspect a problematic response, these are the commands and resources that help:
- curl -i https://api.example.com/endpoint to see headers and body. If you get HTML, search it for clues (authentication page, stack trace, or error template). If the Accept header matters, test with curl -H "Accept: application/json".
- Useful references: MDN on Content-Type and HTTP headers, and common troubleshooting threads on Stack Overflow. If you need a sample error page to debug with, you can reproduce one or review a stored sample such as the provided case: API returning HTML instead of JSON.
Also read about robust error handling libraries and monitoring tools (for example Sentry and Datadog) to capture stack traces and metrics for parse failures.
Semantic core (keyword clusters)
Primary keywords:
- SyntaxError Unexpected token '<'
- handling SyntaxError Unexpected token '<' error
- JSON parsing error handling
- API returning HTML instead of JSON
- checking Content-Type header for JSON
Secondary keywords:
- handling non-JSON API responses
- retry mechanism for API errors
- API monitoring and error handling
- response.content-type application/json
- fetch JSON safe parsing
Clarifying / LSI phrases:
- unexpected token '<' when parsing JSON
- HTML error page instead of JSON response
- content-type mismatch json vs html
- exponential backoff with jitter for retries
- inspect response.text before JSON.parse
- curl check content-type and body
- login redirect returns HTML to API calls
- sanitizing logged response bodies
- circuit breaker for unstable API
- Retry-After header handling
Usage guidance:
- Use primary keywords in title, H1, and early paragraphs
- Use secondary and LSI phrases across subheads and code examples
- Avoid keyword stuffing; prefer natural phrasing and developer terminology
Selected user questions (top 5–10) and final FAQ
Candidate questions found in "People also ask" and forums:
- Why do I get SyntaxError: Unexpected token '<' when parsing JSON?
- How do I check if an API returned HTML instead of JSON?
- How to handle JSON.parse failing in production?
- Should I retry when the API returns HTML or a 5xx?
- How to prevent login pages from being returned to API clients?
- How to log and monitor JSON parse errors without exposing PII?
- What headers guarantee JSON responses?
- How to implement exponential backoff with jitter?
Final FAQ
Q: Why do I see "SyntaxError: Unexpected token '<'" when parsing JSON?
A: Because the parser received HTML (starting with '<') instead of JSON. The server likely returned an error page, auth redirect, or an HTML response. Check response.status and Content-Type, inspect response.text, and log a truncated body for debugging.
Q: How can I safely parse API responses to avoid runtime crashes?
A: Read the response as text, validate response.ok and Content-Type contains application/json, then call JSON.parse inside try/catch (or r.json() guarded by header checks). On mismatch, log the response snippet and throw a clear error so the caller can handle it.
Q: When should I retry a request that returned non-JSON or a server error?
A: Retry on transient errors: 429, 502, 503, 504, and network timeouts. Use exponential backoff with jitter, respect Retry-After headers, cap retries, and use a circuit breaker to avoid persistent retries against broken services. Do not retry on client errors like 400/401/403/404.
Suggested micro-markup
Include FAQ JSON-LD (already present in the page head) and Article schema if desired. For example, keep the FAQ schema to improve chances for rich results in search engines and voice assistants. The JSON-LD at the top of this document implements the three FAQ items above.
Backlinks and further reading
Reference links (anchor text uses keywords):
- Content-Type header for JSON (MDN)
- Common "Unexpected token" troubleshooting (Stack Overflow)
- API returning HTML instead of JSON (sample case)
- Sentry — monitoring for parse failures
Published: Ready-to-publish troubleshooting guide. Use the code patterns and monitoring advice in staging first, sanitize any logs for PII, and add tests to prevent regressions.