Unexpected token '<', "<!DOCTYPE "... is not valid JSON

The first character of the text was <, which means the body is HTML, not JSON. The JSON is not malformed: the request reached something that answered with a web page, usually an error page, a login page or the app’s own index.html. Open the request in the browser’s Network tab and look at the actual response body and status code.

Seen as:

  • SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON
  • SyntaxError: Unexpected token < in JSON at position 0
  • SyntaxError: JSON.parse: unexpected character at line 1 column 1 of the JSON data
  • SyntaxError: JSON Parse error: Unrecognized token '<'
  • requests.exceptions.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

Input

Settings

History

Load from URL

Common causes

1. The URL is wrong and the server sent a 404 page

A typo, a missing version prefix or a path that only exists in another environment returns the server’s HTML 404 page. fetch does not reject on HTTP errors, so check res.ok before parsing.

Before
const res = await fetch('/api/v2/user');
const users = await res.json();
After
const res = await fetch('/api/v2/users');
if (!res.ok) throw new Error(`GET /api/v2/users failed: ${res.status}`);
const users = await res.json();

2. A relative URL resolved against the current page

Without a leading slash, api/users is resolved relative to the page, so on /dashboard/ the request goes to /dashboard/api/users. Single-page apps answer unknown paths with index.html, which starts with <!DOCTYPE html>.

Before
const res = await fetch('api/users');
After
const res = await fetch('/api/users');

3. The dev server has no API proxy

In development, Vite, webpack-dev-server and Create React App serve index.html for any path they do not know. Without a proxy rule, /api calls never reach your backend.

Before
export default {
  server: { port: 5173 },
};
After
export default {
  server: {
    port: 5173,
    proxy: { '/api': 'http://localhost:8080' },
  },
};

4. An expired session redirected to a login page

Many apps answer an unauthenticated API call with a 302 redirect to the login form, and fetch follows it silently. Check the content type so the real problem (the session) is reported instead of a parse error.

Before
const data = await res.json();
After
const type = res.headers.get('content-type') ?? '';
if (!type.includes('application/json')) {
  throw new Error(`Expected JSON but got ${type} from ${res.url}`);
}
const data = await res.json();

Frequently asked questions

How do I see the HTML my code received?

Open the browser developer tools, go to the Network tab, click the request and open the Response panel. In code, call res.text() instead of res.json() and log the first few hundred characters.

Why does the API work in the browser address bar but not in my app?

The address bar sends your cookies and follows redirects visibly, and its URL is absolute. Your app may be calling a relative URL, missing an auth header, or hitting a different origin that returns an error page.

Does this error mean my server is down?

Not necessarily, but it can. Proxies and load balancers such as nginx or Cloudflare return HTML pages for 502 and 503 errors, so a failing backend also produces a < at position 0.

Related