Back

'Unexpected token < in JSON at position 0' エラーの修正

'Unexpected token < in JSON at position 0' エラーの修正

response.json() を呼び出すと、アプリケーションが SyntaxError: Unexpected token '<' in JSON at position 0 でクラッシュします。この < 文字は、何が起こったかを正確に示しています。コードはJSONを期待していましたが、代わりにHTMLを受け取ったのです。

このJSONパースエラーは、モダンなフロントエンドスタック(ブラウザのfetch、Node.js、Next.jsのAPIルート、サーバーレス関数)で頻繁に発生します。なぜ発生するのか、どのように素早くデバッグするのかを理解することで、何時間もの苦労を節約できます。

重要なポイント

  • “Unexpected token <” エラーは、HTMLをJSONとしてパースしようとしていることを意味します。< は通常 <!DOCTYPE html> またはHTMLタグからのものです。
  • 一般的な原因には、誤ったURL、認証リダイレクト、HTMLページを返すサーバーエラー、Content-Typeヘッダーの欠落などがあります。
  • デバッグは、まずHTTPステータスコードを確認し、次にContent-Typeヘッダー、そして response.text() を使用して生のレスポンスボディを確認します。
  • レスポンスをパースする前に検証する防御的なfetchラッパーを構築し、明確なエラーメッセージでこれらの問題をキャッチします。

APIがJSONの代わりにHTMLを返す理由

このエラーは、JSON.parse() が有効なJSONを期待していた場所でHTML文書に遭遇したことを意味します。位置0の < は、通常 <!DOCTYPE html> またはHTMLタグの開始文字です。

このContent-Typeの不一致を引き起こすシナリオはいくつかあります:

誤ったまたはスペルミスのエンドポイントURL。 fetchのURLにタイプミスがあると、404ページが返されます。これはJSONではなくHTMLです。

認証リダイレクト。 期限切れのトークンや認証ヘッダーの欠落により、ログインページへのリダイレクトがトリガーされます。fetchはログインページのHTMLを受け取ります。

HTMLエラーページを返すサーバーエラー。 APIゲートウェイやクラウドプロバイダーからの500エラーは、JSONエラーレスポンスではなく、スタイル付きのHTMLエラーページを返すことがよくあります。

未知のルートに対してフォールバックHTMLを提供する開発サーバー。 多くのSPAは、一致しないパスに対してHTMLシェルを返しますが、一部のモダンな開発サーバーは構造化されたエラーペイロードを返します。

Content-Typeヘッダーの欠落または不正確。 Content-Type: application/json の設定を忘れたサーバーコードは、HTMLにデフォルト設定される可能性があります。

Fetch APIエラーの実践的なデバッグフロー

JSONパースが失敗した場合、根本原因を特定するために次の手順に従います:

ステップ1: HTTPステータスコードを確認する

パースする前に、レスポンスのステータスを確認します。4xxまたは5xxステータスは、レスポンスがJSONではないことを示すことがよくあります:

const response = await fetch('/api/data')

if (!response.ok) {
  console.error(`HTTP ${response.status}: ${response.statusText}`)
  const text = await response.text()
  console.error('Response body:', text.substring(0, 200))
  throw new Error(`Request failed with status ${response.status}`)
}

ステップ2: Content-Typeヘッダーを検証する

サーバーが送信していると主張している内容を確認します:

const contentType = response.headers.get('content-type')

if (!contentType || !contentType.includes('application/json')) {
  const text = await response.text()
  throw new Error(`Expected JSON, received: ${contentType}. Body: ${text.substring(0, 100)}`)
}

const data = await response.json()

ステップ3: 生のレスポンスボディをログに記録する

パースが失敗した場合、response.json() の代わりに response.text() を使用して、実際に受け取った内容を確認します:

async function fetchWithDebug(url) {
  const response = await fetch(url)
  const text = await response.text()
  
  try {
    return JSON.parse(text)
  } catch (error) {
    console.error('Failed to parse JSON. Raw response:', text.substring(0, 500))
    throw error
  }
}

実際によくある落とし穴

本番環境での誤ったAPIベースURL。 環境変数が誤ったドメインを指していたり、末尾のスラッシュが欠けていたりすると、HTMLを返す404が発生します。

リクエストをインターセプトするAPIゲートウェイとCDN。 Cloudflare、AWS API Gateway、Vercelなどのサービスは、レート制限、タイムアウト、または設定ミスに対して独自のHTMLエラーページを返すことがあります。

Next.js App Routerとミドルウェアのリダイレクト。 ミドルウェアまたは認証リダイレクトはHTMLを返すことが多いですが、一部のリダイレクトパスは代わりに小さなJSONペイロードを出力します。

JSONヘッダーが欠落しているサーバーコード。 APIハンドラーはデータを返しますが、レスポンスのコンテンツタイプの設定を忘れています:

// ❌ Content-Typeが欠落
res.send({ data: 'value' })

// ✅ 明示的なJSONレスポンス
res.json({ data: 'value' })

CORSの問題。 ブラウザはコードが実行される前に失敗したプリフライトリクエストをブロックしますが、設定ミスのサーバーまたはプロキシは、fetchコールが受け取るHTMLエラーページを返す可能性があります。

防御的なFetchパターン

これらの問題を早期にキャッチするために、fetchコールを検証でラップします:

async function safeFetch(url, options = {}) {
  const response = await fetch(url, options)
  
  if (!response.ok) {
    const body = await response.text()
    throw new Error(`HTTP ${response.status}: ${body.substring(0, 100)}`)
  }
  
  const contentType = response.headers.get('content-type')
  if (!contentType?.includes('application/json')) {
    const body = await response.text()
    throw new Error(`Invalid content-type: ${contentType}`)
  }
  
  return response.json()
}

まとめ

“Unexpected token <” エラーは、常にHTMLをJSONとしてパースしていることを意味します。デバッグは、まずステータスコードを確認し、次にContent-Typeヘッダー、そして生のレスポンスボディを確認します。ほとんどのケースは、誤ったURL、認証リダイレクト、またはHTMLページを返すサーバーエラーに起因します。パースする前にレスポンスを検証する防御的なfetchラッパーを構築し、明確なエラーメッセージでこれらの問題をキャッチしましょう。

よくある質問

本番環境には異なる設定があることがよくあります。APIベースURLの環境変数が正しく設定されているか確認し、認証トークンが有効であることを確認し、フロントエンドとAPIの間にあるプロキシやCDNが適切に設定されていることを確認してください。本番サーバーは、リクエストを失敗させるより厳格なCORSポリシーを持っている場合もあります。

はい。response.json()を呼び出す前にレスポンスのステータスとContent-Typeヘッダーを確認する防御的な関数でfetchコールをラップします。これにより、エラーを適切に処理し、クラッシュする代わりにユーザーに意味のあるメッセージを表示できます。パースする前に常にレスポンスを検証してください。

ブラウザのNetworkタブを使用して実際のレスポンスを検査します。レスポンスが404または500ステータスのHTMLコンテンツを示している場合、サーバーがエラーページを返しています。ステータスが200だがコンテンツがHTMLの場合は、サーバーコードを確認して、正しいContent-Typeヘッダーを設定し、JSONを返していることを確認してください。

response.json()メソッドは、レスポンスボディをJSONとしてパースしようとします。ボディにHTMLまたは非JSON コンテンツが含まれている場合、パースは失敗します。response.text()メソッドは、パースせずに生のレスポンスを単に文字列として返すため、コンテンツタイプに関係なく機能します。実際に受け取った内容を確認するために、デバッグにはtext()を使用してください。

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay