← 全部文章

JSON 第 0 位 Unexpected Token <:你拿到的是 HTML

「Unexpected token <」錯誤代表 JSON.parse 收到了一個 HTML 頁面(404、登入跳轉或錯誤的 URL),而不是 JSON。本文說明原因,並附上壞掉與修好的 fetch 範例。

SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON —— 幾乎總是代表一件事:你的程式碼對一個 HTML 頁面(不是 JSON)呼叫了 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 呼叫回傳了 HTML 文件,而不是你預期的 JSON。常見來源:

  • 你的框架或 Web 伺服器以 HTML 形式提供的一個 404 / 500 錯誤頁
  • 跳到 HTML 登入頁的 登入或驗證重新導向(工作階段過期)
  • 用了錯誤的 URL —— 打到了 app/SPA 路由而非 API 端點
  • 一個代理、CDN 或 captive portal 回傳了自己的 HTML 頁

壞掉的例子

const res  = await fetch('/api/user');   // 伺服器回了 404 HTML 頁
const data = await res.json();           // ❌ Unexpected token '<' …
// res.json() 試圖把 "<!DOCTYPE html>…" 當成 JSON 來解析

修好的例子

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

// 1) 解析前先檢查狀態
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();           // ✅ 安全

另外兩個比較不顯眼但值得知道的來源

  • WAF 攔截或速率限制的回應。 一個 WAF、CDN 或 DDoS 防護層可能攔住呼叫並回傳自己的 HTML 挑戰 / 攔截頁 —— Cloudflare 的中間頁、AWS WAF 的拒絕頁、公共 Wi-Fi 的 captive-portal 牆。狀態碼常是 403 / 429 / 503 加上 HTML body。在 Network 面板中檢查回應,看是否有 CF-Ray X-Amz-Cf-Id 這類標頭可以辨識。
  • SPA 歷史回退回傳 index.html 靜態主機與開發伺服器常將每個未知路徑改寫成 /index.html,好讓客戶端路由器接手。如果你的前端不小心用相對路徑 fetch 了 /api/user,而代理沒設定好,伺服器會靜靜地以 200 回傳你 SPA 的 HTML 殼 —— 你會拿到一個令人困惑的「200 OK 但是 HTML」版本的同一個錯誤。

怎麼修 —— 一步一步

  1. 把原始 body 印出來:console.log(await res.clone().text())。如果開頭是 <!DOCTYPE<html>,就是 HTML。
  2. 在呼叫 res.json() 之前檢查 res.ok / res.status —— 4xx/5xx 通常會回傳 HTML 錯誤頁。
  3. 在 Network 面板確認 URL。相對路徑可能解析成你 SPA 的 index.html,而不是 API。
  4. 檢查驗證 —— 重新導向到登入頁代表你的 token/工作階段過期了。
  5. 檢查 Content-Type —— 依此分支處理,避免對非 JSON 回應盲目解析。

常見問題

「Unexpected token < in JSON at position 0」是什麼意思?

解析器把 < 當成第一個字元,這在任何合法 JSON 中都不可能。回應其實是 HTML(一個 <!DOCTYPE><html> 頁面),而非你程式碼預期的 JSON。

為什麼我的 fetch 回傳的是 HTML 而非 JSON?

常見原因:404/500 錯誤頁、跳轉到登入頁的驗證重新導向,或 URL 寫錯,被解析到前端而非 API。檢查 res.ok 並列印 await res.text() 看看回了什麼。

怎麼避免它讓我的應用崩潰?

解析前加 guard:檢查 res.okContent-Type 標頭,把 JSON.parse()/res.json() 包在 try/catch 裡,讓 HTML 回應變成可處理的錯誤,而非例外。

立刻修復

如果有一段回應 body 你拿不準,貼到 JSON Fix —— 它會立刻告訴你那是合法 JSON 還是其他東西(例如 HTML)。所有運算都在瀏覽器中執行。