← 記事一覧

JSON の位置 0 で Unexpected Token <: 来たのは HTML

"Unexpected token <" エラーは、JSON.parse が JSON ではなく HTML ページ (404、ログインリダイレクト、誤った URL) を受け取ったということ。原因を、壊れた/修正後の fetch 例で解説。

SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON —— ほぼ常に意味するのは一つ:あなたのコードが JSON ではなく HTML ページ に対して JSON.parse()(または res.json())を呼んだ、ということです。位置 0 の <<!DOCTYPE html><html> の開始です。なぜ起きるかと、どう直すかを正確に説明します。

エラーの見え方

// V8 (Chrome / Node / Edge)
SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON
// より古い V8
SyntaxError: Unexpected token < in JSON at position 0

// Firefox
SyntaxError: JSON.parse: unexpected character at line 1 column 1 of the JSON data

// Safari
SyntaxError: JSON Parse error: Unexpected identifier "<"

位置 0 が鍵:最初のバイトが既に間違っているため、レスポンスは最初から JSON ではありませんでした。

なぜ起きるか:HTML を受け取った

fetch または AJAX 呼び出しが、期待していた JSON ではなく HTML ドキュメントを返しました。一般的な原因:

  • フレームワークや Web サーバが HTML として配信する 404 / 500 エラーページ
  • HTML のサインインページへの ログイン/認証リダイレクト(セッション切れ)
  • URL の誤り —— API エンドポイントではなく app/SPA ルートに当たっている
  • プロキシ、CDN、キャプティブポータル が独自の HTML ページを返した

壊れた例

const res  = await fetch('/api/user');   // サーバが 404 HTML ページを返した
const data = await res.json();           // ❌ Unexpected token '<' …
// res.json() が "<!DOCTYPE html>…" を JSON として parse しようとする

修正後の例

const res = await fetch('/api/user');

// 1) parse 前にステータスを確認
if (!res.ok) {
  throw new Error(`HTTP ${res.status} ${res.statusText}`);
}

// 2) オプション:本当に JSON か確認
const type = res.headers.get('content-type') ?? '';
if (!type.includes('application/json')) {
  const text = await res.text();
  throw new Error(`Expected JSON, got: ${text.slice(0, 80)}…`);
}

const data = await res.json();           // ✅ 安全

知っておくと役立つ、目立たない 2 つの原因

  • WAF がブロック/レート制限したレスポンス。 WAF、CDN、DDoS 防御層が呼び出しを横取りし、自身の HTML チャレンジ/ブロックページを返すことがあります —— Cloudflare のインタースティシャル、AWS WAF の拒否ページ、公衆 Wi-Fi のキャプティブポータル。ステータスは 403 / 429 / 503 と HTML ボディの組み合わせがよくあります。Network タブでレスポンスを確認し、CF-Ray X-Amz-Cf-Id のようなヘッダで気づけます。
  • SPA のヒストリーフォールバックが index.html を返す。 静的ホストや開発サーバは、クライアントルータに引き継がせるため未知のパスをすべて /index.html に書き換えるのが一般的です。フロントエンドがうっかり /api/user を相対パスで fetch し、プロキシ設定がないと、サーバは SPA の HTML シェルを 200 で静かに返します —— 「200 OK だけど HTML」というややこしい版の同じエラーになります。

修正の手順

  1. 生のボディをログ:console.log(await res.clone().text())<!DOCTYPE<html> で始まっていれば、それは HTML。
  2. res.json() を呼ぶ前に res.ok / res.status を確認 —— 4xx/5xx は通常 HTML のエラーページを返します。
  3. Network タブで URL を確認。 相対パスは API ではなく SPA の index.html に解決されることがあります。
  4. 認証を確認 —— ログインページへのリダイレクトはトークン/セッションが切れた合図。
  5. Content-Type を検査 —— それで分岐し、非 JSON レスポンスを盲目的に parse しないように。

よくある質問

「Unexpected token < in JSON at position 0」とは?

パーサが < を最初の文字として見つけた。これは合法な JSON では決してありません。レスポンスはコードが期待した JSON ではなく HTML(<!DOCTYPE><html> ページ)でした。

なぜ fetch が JSON ではなく HTML を返すのか?

よくある原因:404/500 のエラーページ、ログインページへの認証リダイレクト、または API ではなくフロントエンドに解決される誤った URL。res.ok を確認し、await res.text() をログして何が返ってきたか確認。

アプリが落ちないようにするには?

parse 前にガード:res.okContent-Type ヘッダをチェックし、JSON.parse()/res.json() を try/catch で包む。HTML レスポンスは例外でなく扱えるエラーになります。

今すぐ直す

自信のないレスポンスボディがあれば、JSON Fix に貼り付けてください —— 有効な JSON か、別の何か(HTML など)かをすぐに判定します。すべてブラウザで動作。