# SyntaxError: “Unexpected token < in JSON at position 0”

> SyntaxError: Unexpected token < in JSON at position 0. The server sent HTML, not JSON: check the URL, the status code and the API route.

Canonical: https://app369.com/fix/unexpected-token-in-json/
Last updated: 2026-11-07

A fetch call fails with SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON (older Chrome: Unexpected token < in JSON at position 0; Firefox: JSON.parse: unexpected character at line 1 column 1 of the JSON data; Safari: JSON Parse error: Unrecognized token '<'). The code called res.json() or JSON.parse() on a response that starts with <, which is an HTML page, not JSON. The real problem is whatever made the server send HTML: a wrong URL, an error page, a login redirect or a hosting rewrite.

**Short answer**

Open the Network tab, click the failing request and look at the response: it will be an HTML page. If it is your site’s index.html, the URL is wrong or a single-page app rewrite catches /api routes before your API does. If it is a 404, 500 or login page, fix that first. In code, check res.ok and the content-type before calling res.json(), and log await res.text() when it fails so you can see what came back.

## **Why the response is HTML, and the fix**

1. 01 ### **The request got an error page**
  > “A fetch() promise does not reject if the server responds with HTTP status codes that indicate errors (404, 504, etc.).” [**MDN, Window: fetch() method**](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch) fetch() resolves for 404 and 500 responses too, so the code goes on to parse the server’s HTML error page as JSON. Check res.ok before reading the body, and show the status when it fails.```
const res = await fetch('/api/orders');
if (!res.ok) {
  throw new Error(\`API ${res.status}: ${(await res.text()).slice(0, 200)}\`);
}
const data = await res.json();
```
2. 02 ### **The URL points at the front end, not the API** A relative URL like /api/users goes to the site that served the page. In development that is often the Vite or React dev server, which answers unknown paths with index.html. Use the full API URL from an environment variable, or set up the dev server proxy. In production, check the deployed API base URL is set.```
// vite.config.js: send /api to the backend in development
export default {
  server: { proxy: { '/api': 'http://localhost:4000' } },
};
```
3. 03 ### **A hosting rewrite catches the API route** Single-page apps on Netlify, Vercel or Firebase Hosting rewrite every path to /index.html so client routing works. If a request to /api/... matches that catch-all instead of a real API route, you get the HTML page with status 200. On Netlify and Firebase Hosting, rules run in order, so put the API or function rule before the catch-all. On Vercel, a file in the api folder wins over a rewrite, so the HTML means the route does not exist: check the file name, the path and that the function deployed.
4. 04 ### **A login redirect or a file that is not JSON**
  > “This string has to be valid JSON and will throw this error if incorrect syntax was encountered.” [**MDN, SyntaxError: JSON.parse: bad parsing**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/JSON_bad_parse) A request without a valid session can be redirected to a login page, and fetch follows the redirect and gets HTML. Check that the auth header or cookie is sent. When you parse a file or string yourself, make sure it really is JSON: no trailing commas, double quotes around names and strings.

Checked against the vendor’s docs on November 7, 2026: [MDN: Window: fetch() method (HTTP errors do not reject)](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch); [MDN: Response: ok property](https://developer.mozilla.org/en-US/docs/Web/API/Response/ok); [MDN: SyntaxError: JSON.parse: bad parsing](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/JSON_bad_parse).

## **Rather hand it over?**

Fixed scope, fixed price, paid upfront. The work happens in writing; no call needed.

- [from $450**App Fix Ticket**One reproducible bug in an existing mobile or web app, diagnosed and fixed.**See scope and start**](https://app369.com/fixed-price/flutter-firebase-bug-fix/)
- [from $450**AI-Built App Rescue**A Lovable, Bolt or Cursor app checked end to end in 3 days, then its worst problems fixed at a fixed price.****No issues worth fixing, no charge.**** If the Launch Check finds nothing rated medium or higher, Simon refunds the full $450. You keep the report either way. This applies to the first 10 Launch Checks bought after October 30, 2026.**See scope and start**](https://app369.com/fixed-price/ai-app-rescue/)

Related: [**Page not found on refresh (Netlify, Vercel)**](https://app369.com/fix/page-not-found-on-refresh-netlify-vercel/) · [**“Blocked by CORS policy”**](https://app369.com/fix/blocked-by-cors-policy/) · [**“Cannot read properties of undefined”**](https://app369.com/fix/cannot-read-properties-of-undefined/)

Questions

## **More on this error**

<details>

<summary>**The API works in Postman but not in the browser. Why? **+</summary>

Postman calls the API URL directly. The browser code may be calling a relative URL on the front end, sending no auth cookie, or hitting a hosting rewrite. Compare the exact request URL and headers in the Network tab with the ones in Postman.

</details>

<details>

<summary>**What does “Unexpected end of JSON input” mean? **+</summary>

The response body was empty, for example a 204 No Content or a server that crashed before writing. Check the status first and only call res.json() when there is a body to parse.

</details>

<details>

<summary>**Can you fix it for me? **+</summary>

If one bug is the whole problem, the App Fix Ticket covers it: a written diagnosis first, then the fix. If the app has more going on, the Launch Check is a written check of the whole app. Each issue found is rated critical, high, medium or low, with where it is and how it was confirmed. No issues worth fixing, no charge. If the Launch Check finds nothing rated medium or higher, Simon refunds the full $450. You keep the report either way. This applies to the first 10 Launch Checks bought after October 30, 2026.

</details>

A different error?

## **Describe it and get a written quote**

Send the error and what you were doing. You get a written scope, a fixed price and a timeline; Simon replies by email, usually within one business day.

[**Describe your problem **→](https://app369.com/contact/) [**All fixes**](https://app369.com/fix/)