SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON は、JSON が返ってくるはずの API から、HTML が返ってきたときに出るエラーです。 < は HTML の最初の文字(<!DOCTYPE html> の <)です。JavaScript のコードが間違っているのではなく、リクエストの行き先か、サーバーの応答に原因があります。

エラー文 返ってきたもの よくある原因
Unexpected token '<', "<!DOCTYPE "... is not valid JSON HTML 404 ページ、ログイン画面、SPA の index.html
Unexpected token '<', "<br />\n<b>"... is not valid JSON PHP の警告文+JSON サーバー側の PHP が警告を出力した
Unexpected token 'I', "Internal S"... is not valid JSON テキストのエラー文 サーバー側のエラー(500)
Unexpected end of JSON input 空の本文 204 No Content、本文の無いエラー応答
Unexpected token < in JSON at position 0 HTML 同じエラーの以前の表記(Node.js 18 まで・以前の Chrome)

この記事では、それぞれを実際に起こし、確かめ方と直し方を並べました。

確認環境:Node.js 22.22.2(fetch)、Chromium 141、axios 1.20.0、Vite 8.3.4(確認日 2026年10月10日)。テスト用に、パスごとに違う応答を返す小さなサーバーを作って確認しました。表記の比較には Node.js 16・18・20 も使っています。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Unexpected token '<' in JSONの原因と直し方|fetch」

まず「何が返ってきたか」を見る

エラーの文言だけでは、なぜ HTML が返ったかは分かりません。ブラウザの開発者ツール(F12)の Network タブで、そのリクエストを確かめます。

見るところ 分かること
Status 404 なら URL の間違い、302・301 ならリダイレクト、500 ならサーバーのエラー
Response Headers の Content-Type text/html なら HTML が返っている
Request URL 意図した API のサーバー・パスになっているか(開発サーバーに向いていないか)
Response(Preview)タブ 返ってきた中身。404 ページかログイン画面か、ひと目で分かる

Node.js で確かめるときは、res.json() の前に中身を表示します。

const res = await fetch(url);
console.log(res.status, res.headers.get('content-type'), res.url);
console.log((await res.text()).slice(0, 200));   // 先頭200文字

res.url には、リダイレクトされた後の URL が入ります。ログイン画面に飛ばされていれば、ここで分かります。

原因と直し方:7つの例

同じ fetch(...).then(res => res.json()) で、行き先だけを変えた結果です。

/api/users   OK [{"id":1,"name":"山田"}]
/api/user    SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON   ← URL の間違い(404)
/api/secure  SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON   ← ログイン画面へリダイレクト
/api/error   SyntaxError: Unexpected token 'I', "Internal S"... is not valid JSON   ← 500 エラー
/api/empty   SyntaxError: Unexpected end of JSON input                             ← 204(本文なし)
/api/php     SyntaxError: Unexpected token '<', "<br />
<b>"... is not valid JSON                                                         ← PHP の警告
/users       SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON   ← SPA の index.html

例1:URL が違っていて 404 ページが返った

/api/users を /api/user と書いたなど、存在しないパスを呼ぶと、サーバーの 404 ページ(HTML)が返ります。Network タブで Status が 404 になっていれば、これです。パス・ベースURL・末尾のスラッシュを確認します。

例2:ログインの期限が切れて、ログイン画面が返った

認証が必要な API で、セッションやトークンの期限が切れていると、ログイン画面にリダイレクト(302)され、ログイン画面の HTML が 200 で返ります。fetch はリダイレクトを自動でたどるため、Status は 200 に見えます。res.url が /login などになっていれば、これです。

直し方 内容
サーバー側 API へのリクエスト(/api/ 配下や Accept: application/json)には、リダイレクトではなく 401 を返すようにする
クライアント側 401 を受け取ったらログイン画面に移す処理を入れる

例3:開発サーバーの proxy を設定していない(Vite など)

フロントエンドを Vite で動かし、API を別のサーバー(Spring Boot など)で動かしているとき、fetch('/api/users') は Vite の開発サーバーに届きます。Vite は、知らないパスへのリクエストに index.html を返します。

$ curl -H "Accept: */*" http://localhost:5173/api/users
(index.html の中身)  ← 200 text/html

$ curl -H "Accept: application/json" http://localhost:5173/api/users
                       ← 404

fetch の既定の Accept は */* なので、エラーではなく index.html が 200 で返り、res.json() で初めて失敗します。vite.config.js に proxy を書くと、API サーバーに転送されます。

// vite.config.js
import { defineConfig } from 'vite';

export default defineConfig({
  server: {
    proxy: {
      '/api': 'http://127.0.0.1:8080',   // /api で始まるリクエストを API サーバーへ転送
    },
  },
});
$ curl http://localhost:5173/api/users
[{"id":1,"name":"山田"}]  ← 200 application/json
開発環境 設定する場所
Vite(React・Vue) vite.config.js の server.proxy
Angular CLI proxy.conf.json と ng serve --proxy-config
Next.js next.config.js の rewrites、または API のURLを環境変数で指定

本番では proxy が無いので、本番でも同じパスで API に届くよう、Web サーバー(nginx など)の設定か、API の URL を環境変数で切り替える仕組みが要ります。

例4:本番で SPA の index.html が返った

本番の Web サーバーで、「知らないパスはすべて index.html を返す」設定(SPA のフォールバック)にしていると、API への転送設定が漏れたパスにも index.html が返ります。例3と同じく 200 で返るため、Content-Type が text/html かどうかで見分けます。nginx なら、location /api/ の転送設定がフォールバックより優先されているかを確認します。

例5:サーバーのエラー(500)

サーバー側で例外が起きると、Internal Server Error のようなテキストや、エラーページの HTML が返ります。クライアントでは直せないので、サーバーのログを確認します。クライアントは、res.ok を確かめてから res.json() を呼ぶようにすると、エラーの種類が分かりやすくなります(後の完成コード)。

例6:PHP の警告が JSON の前に出力された

<br />
<b>Warning</b>:  Undefined variable $x in /var/www/api.php on line 3<br />
{"ok":true}

PHP の display_errors が有効だと、警告やお知らせ(Notice)が JSON の前に出力されて、JSON として読めなくなります。Content-Type は application/json のままなので、Network タブの Response で中身を見て気づくことが多い原因です。警告の原因を直し、本番では display_errors=Off(ログにだけ出す)にします。

例7:本文が空(Unexpected end of JSON input)

204 No Content や、本文の無いエラー応答に res.json() を呼ぶと、Unexpected end of JSON input になります。削除・更新の API で 204 を返す場合は、204 のときに res.json() を呼ばないようにします。

axios では別の形で表に出る

axios は、JSON として読めない応答をエラーにせず、文字列のまま res.data に入れます(既定の設定)。例3・4のように index.html が 200 で返ると、後で別のエラーになりました。

/users       200 string "<!DOCTYPE html><html><head><title>App</
/users       TypeError: res.data.map is not a function

res.data.map is not a function や Cannot read properties of undefined が、API を呼んだ直後の行で出たら、この記事の原因を疑います。typeof res.data が 'string' なら HTML が返っています。404・500 は axios が Request failed with status code 404 のようなエラーにするため、こちらは分かりやすく出ます。

完成コード:原因が分かる fetchJson

res.ok・Content-Type・204 を確かめてから JSON を読む関数です。JSON 以外が返ったときに、ステータス・URL・中身の先頭を持ったエラーにします。

export class ApiError extends Error {
  constructor(message, { status, url, contentType, bodyHead }) {
    super(message);
    this.name = 'ApiError';
    Object.assign(this, { status, url, contentType, bodyHead });
  }
}

/** JSON を返すはずの API を呼ぶ。JSON 以外が返ったら、原因が分かるエラーにする */
export async function fetchJson(url, options = {}) {
  const res = await fetch(url, { headers: { Accept: 'application/json', ...options.headers }, ...options });
  const contentType = res.headers.get('content-type') ?? '';
  if (res.status === 204) return null;                         // 本文なし
  const text = await res.text();
  const info = { status: res.status, url: res.url, contentType, bodyHead: text.slice(0, 120) };
  if (!res.ok) {
    throw new ApiError(`HTTP ${res.status}(${res.url})`, info);
  }
  if (!contentType.includes('json')) {
    throw new ApiError(`JSON ではない応答(${contentType || 'Content-Type なし'}):${res.url}`, info);
  }
  try {
    return JSON.parse(text.replace(/^/, ''));              // 先頭の BOM は取り除く
  } catch (e) {
    throw new ApiError(`JSON として読めない応答:${e.message}`, info);
  }
}

7つの例で呼んだ結果です。エラーの文言から、何が起きたかが分かるようになりました。

/api/users   OK [{"id":1,"name":"山田"}]
/api/user    ApiError: HTTP 404(http://127.0.0.1:18181/api/user)
/api/secure  ApiError: JSON ではない応答(text/html; charset=utf-8):http://127.0.0.1:18181/login
/api/error   ApiError: HTTP 500(http://127.0.0.1:18181/api/error)
/api/empty   OK null
/api/php     ApiError: JSON として読めない応答:Unexpected token '<', "<br />
<b>"... is not valid JSON
/users       ApiError: JSON ではない応答(text/html; charset=utf-8):http://127.0.0.1:18181/users

Accept: application/json を付けているので、例3の Vite の開発サーバーでも index.html ではなく 404 が返り、原因に気づきやすくなります。JSON.parse は先頭に BOM があると Unexpected token '' で失敗するため、取り除いてから読んでいます(res.json() は BOM を取り除いて読みました)。

エラーの表記の違い

実行環境 表記
Node.js 20 以降・現在の Chrome・Edge Unexpected token '<', "<!DOCTYPE "... is not valid JSON
Node.js 18 まで・以前の Chrome Unexpected token < in JSON at position 0
Firefox JSON.parse: unexpected character at line 1 column 1 of the JSON data の形

どれも「JSON の1文字目から読めない」という同じエラーです。position 0・line 1 column 1 は、最初の文字から JSON ではないことを表しています。

よくある質問

Q. Postman や curl では JSON が返るのに、ブラウザからだけエラーになります。 A. ブラウザからのリクエストは、開発サーバー(Vite など)に届いている(例3)か、Cookie が付かずにログイン画面に飛ばされている(例2)ことが多い原因です。Network タブの Request URL と Status を、Postman の結果と比べます。

Q. ローカルでは動くのに、本番だけエラーになります。 A. 開発サーバーの proxy は本番には無いため、本番の Web サーバーで API への転送が設定されていないと、index.html が返ります(例4)。本番の URL で Network タブを確認します。

Q. res.json() の前に res.ok を確かめるだけで防げますか。 A. 例2・3・4では Status が 200 のまま HTML が返るため、res.ok だけでは防げません。Content-Type も確かめます。

まとめ

  • Unexpected token '<' は、JSON の代わりに HTML が返ったエラー。コードではなく行き先・応答を調べる
  • Network タブで Status・Content-Type・Request URL・Response を見る。res.url でリダイレクトも分かる
  • 200 のまま HTML が返る原因が多い:ログイン画面へのリダイレクト、Vite の proxy 漏れ、本番の SPA フォールバック
  • Unexpected end of JSON input は空の本文。204 では res.json() を呼ばない
  • res.ok・Content-Type を確かめてから読む fetchJson を使うと、原因が分かるエラーになる

参考・出典

確認日はいずれも 2026年10月10日です。

  • MDN Web Docs「SyntaxError: JSON.parse: bad parsing」:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/JSON_bad_parse
  • Vite「Server Options(server.proxy)」:https://vite.dev/config/server-options

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Unexpected token '<' in JSONの原因と直し方|fetch」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?