- 技術 💻
Unexpected token ‘<' in JSONの原因と直し方|fetch
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期連続の黒字決算。