Runtime / API error

Fix 'Unexpected token < in JSON'

The short version: you parsed something that is not JSON. That leading < is the first character of an HTML page (<!DOCTYPE html> or <html>) - the server returned an error page, a 404, or a login redirect, and your code called res.json() on it. Do not fix the parser; find out what the response actually was: log res.status and the raw res.text() before parsing, and fix why the server sent HTML.

Not your exact error? Paste it into the Deploy Error Decoder →

See what actually came back

Stop parsing blindly. Log the status and the raw body first - the answer is almost always right there:

const res = await fetch(url);
if (!res.ok || !res.headers.get("content-type")?.includes("application/json")) {
  const body = await res.text();
  throw new Error(`Expected JSON, got ${res.status}: ${body.slice(0, 120)}`);
}
const data = await res.json();

If that body starts with <!DOCTYPE html>, you have your answer: the server sent a web page, not data.

Why HTML came back

The endpoint errored

A 500 or unhandled exception rendered the framework HTML error page instead of a JSON body.

Wrong URL

You hit the frontend or a catch-all 404 page - a missing /api prefix, a bad base URL, or a typo in the path.

Auth redirect

An unauthenticated request was redirected to an HTML login page instead of returning 401 JSON.

Proxy / gateway page

A reverse proxy returned its own HTML 502/504 page because the app behind it was down or slow.

The principle

The parser is the messenger, not the bug

It is tempting to wrap JSON.parse in a try/catch and move on - but that swallows the real signal. An HTML body where you expected JSON is the upstream telling you something failed: it errored, you asked the wrong thing, or auth rejected you. Surface the status and the first part of the body, and the cryptic JSON exception becomes an obvious “the API returned a 502 page.”

If the body is a proxy error page, the real fix is upstream - see the 502 and 504 guides. Paste a snippet into the JSON formatter to confirm what you actually received.

How Infraveil helps

See the real response, not the parser’s complaint

This error is a layer away from its cause - the parser fails, but the problem is what the upstream sent. On your own servers, Infraveil traces the actual request and response between your services - the status, the content-type, what the upstream really returned - so “Unexpected token <” becomes “the orders API has been returning its 500 page since 14:02,” recorded and inspectable.

Frequently asked questions

What does “position 0” mean?

The very first character is already invalid - the parser failed immediately. JSON never starts with <, so a < at position 0 is the start of an HTML document. A different position usually means truncated or malformed JSON instead.

Why did the server send HTML instead of JSON?

Usually: the endpoint errored and rendered an HTML error page (500/404), you hit the wrong URL (frontend or catch-all 404), an auth layer redirected to a login page, or a proxy returned its own 502/504 HTML. The status code and raw body tell you which.

How do I handle this safely in code?

Check res.ok and that content-type is application/json before parsing. If not JSON, read res.text() and surface it - an unexpected HTML body is information. Parsing blindly turns a clear upstream error into a cryptic exception layers away.

It works locally but fails in production - why?

The URL or upstream differs: a relative path now hitting a 404 page, an env var pointing at the wrong host, or a proxy returning HTML where dev returned JSON. Log the full URL and raw response in the failing environment - it is rarely the parser.