🐍

【Next.js】【エラー】TypeError: fetch failed

2025/01/12に公開

Vercel に Next.js をデプロイしたときに「TypeError: fetch failed」がでて

デプロイに失敗しました。

解決していきまーーーす!

TypeError: fetch failed ①

エラー内容確認

Vercel でデプロイログを確認しました

TypeError: fetch failed
    at Object.fetch (node:internal/deps/undici/undici:11576:11)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
  cause: Error: connect ECONNREFUSED 127.0.0.1:3000
      at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1495:16)
      at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
    errno: -111,
    code: 'ECONNREFUSED',
    syscall: 'connect',
    address: '127.0.0.1',
    port: 3000
  }
}
Error occurred prerendering page "/". Read more: https://nextjs.org/docs/messages/prerender-error
TypeError: fetch failed
    at Object.fetch (node:internal/deps/undici/undici:11576:11)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)

fetch で Route Handlers を呼び出したときにエラーになってるっぽい

ググってみる

TypeError: fetch failed」「ECONNREFUSED」ここら辺の単語でググってみました

https://github.com/node-fetch/node-fetch/issues/1624

https://github.com/vercel/next.js/issues/44062

↑ の記事によると、

Node.js 17/18 では localhost が IPv6 で名前解決されることが原因みたいです。

Vercel で Project Settings > General > Node.js Versionを確認してみると、

確かに 18 系っぽい。。

ただ、デプロイログを再度見てみると 127.0.0.1 で解決できているので原因は別にありそう

まあ、一応試してみます。

コード修正

たたいている API の URL をlocalhost ⇒ 127.0.0.1 へ変更し IPv4 を直書きしてみます

// 変更前
const url = "http://localhost:3000/api/user";

↓
// 変更後
const url = "http://127.0.0.1:3000/api/user";

再デプロイ

修正したコードを GitHub に push して再デプロイしてみます。

失敗しました。。。

デプロイログを見てみると、エラーの内容変わらずでした。

TypeError: fetch failed
    at Object.fetch (node:internal/deps/undici/undici:11576:11)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
  cause: Error: connect ECONNREFUSED 127.0.0.1:3000
      at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1495:16)
      at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
    errno: -111,
    code: 'ECONNREFUSED',
    syscall: 'connect',
    address: '127.0.0.1',
    port: 3000
  }
}

うーーん、なんでしょう??

TypeError: fetch failed ②

エラー内容確認

fetch failed なので、やっぱり API がたたけないことが原因ですよね。。

localhost とか 127.0.0.1 ではなく、Vercel のデプロイ用の URL じゃないとダメなのか??

試してみます

Vercel のデプロイ用の URL とは?

Vercel でデプロイしたときに発行される URLのこと。Git でいうコミット ID ぐらいの感覚。

この値は Vercel で設定されているVERCEL_URLという環境変数から取得できます

Vercel で設定されている環境変数については ↓ をご覧ください

https://vercel.com/docs/projects/environment-variables/system-environment-variables

今回はClient Component でもこの環境変数を使用したいので、

NEXT_PUBLIC_VERCEL_URLを使います

コード修正

API の URL を環境変数から生成するようにします

(変更前)

// APIのURL
const url = "http://localhost:3000/api/user";
// APIへリクエスト
const res = await fetch(url, {
  cache: "no-store",
});

(変更後)

  1. .env ファイルに NEXT_PUBLIC_API_PREFIX と NEXT_PUBLIC_VERCEL_URL を定義
  2. 環境変数を読み込む src/lib/config.ts を作成
  3. API の URL を config.ts ファイルを読み込み生成

という 3 つのステップで修正します。

1..env ファイルに NEXT_PUBLIC_API_PREFIX と NEXT_PUBLIC_VERCEL_URL を定義

NEXT_PUBLIC_API_PREFIX="http://"
NEXT_PUBLIC_VERCEL_URL="localhost:3000"

NEXT_PUBLIC_VERCEL_URL では、https:// が取得できないので、NEXT_PUBLIC_API_PREFIX として定義しておきます。

2.環境変数を読み込む src/lib/config.ts を作成

export const config = {
  apiPrefix: process.env.NEXT_PUBLIC_API_PREFIX ?? "http://",
  apiHost: process.env.NEXT_PUBLIC_VERCEL_URL ?? "localhost:3000",
};

3.API の URL を config.ts ファイルを読み込み生成

import { config } from "@/lib/config";

// APIのURL
const url = config.apiPrefix + config.apiHost + "/api/user";
// APIへリクエスト
const res = await fetch(url, {
  cache: "no-store",
});

環境変数から API の URL を指定すると、undefined 同士の足し算でエラーになります
なので、config.ts でデフォルト値を指定し string 型にして足しています

ビルド設定修正

NEXT_PUBLIC_VERCEL_URL は Vercel で設定されているのですが
NEXT_PUBLIC_API_PREFIX はあらかじめ設定しておく必要があります

Project Settings  > Environment Variablesにて
Key  :NEXT_PUBLIC_API_PREFIX
Value :https://

で環境変数を設定します

再デプロイ

修正したコードを GitHub に push して再デプロイしてみます。

失敗しました。。。

デプロイログを見てみると、エラーの内容変わりました

SyntaxError: Unexpected token < in JSON at position 0
    at JSON.parse (<anonymous>)
    at parseJSONFromBytes (node:internal/deps/undici/undici:6662:19)
    at successSteps (node:internal/deps/undici/undici:6636:27)
    at node:internal/deps/undici/undici:1236:60
    at node:internal/process/task_queues:140:7
    at AsyncResource.runInAsyncScope (node:async_hooks:203:9)
    at AsyncResource.runMicrotask (node:internal/process/task_queues:137:8)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)

SyntaxError: Unexpected token < in JSON at position 0

エラー内容確認

SyntaxError: Unexpected token < in JSON at position 0
    at JSON.parse (<anonymous>)
    at parseJSONFromBytes (node:internal/deps/undici/undici:6662:19)
    at successSteps (node:internal/deps/undici/undici:6636:27)
    at node:internal/deps/undici/undici:1236:60
    at node:internal/process/task_queues:140:7
    at AsyncResource.runInAsyncScope (node:async_hooks:203:9)
    at AsyncResource.runMicrotask (node:internal/process/task_queues:137:8)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)

JSON だと思ったけど1文字目が<だから、JSON じゃなくない??

というエラーです。

1文字目が<のため、おそらく API から HTML が返ってきていると思います。

API からのレスポンスヘッダーを確認する

↓ のようにヘッダを出力させます

import { config } from "@/lib/config";

// APIのURL
const url = config.apiPrefix + config.apiHost + "/api/user";
// APIへリクエスト
const res = await fetch(url, {
  cache: "no-store",
});

// レスポンスヘッダを出力する
console.log(res.headers);

// レスポンスボディを取り出す
const data = await res.json();

このコードでデプロイしてみるとログがこんな感じでした

HeadersList {
  cookies: null,
  [Symbol(headers map)]: Map(23) {
    'cache-control' => {
      name: 'Cache-Control',
      value: 'public, max-age=0, must-revalidate'
    },
    'connection' => { name: 'Connection', value: 'keep-alive' },
    'content-encoding' => { name: 'Content-Encoding', value: 'br' },
    'content-security-policy' => {
      name: 'Content-Security-Policy',
      value: "default-src 'self' vercel.com *.vercel.com vercel.live instant-preview-site.vercel.app instant-preview-site-bc8byjwcq.vercel.sh;script-src 'self' 'unsafe-eval' 'unsafe-inline' va.vercel-scripts.com vercel.com *.vercel.com vercel.live instant-preview-site.vercel.app instant-preview-site-bc8byjwcq.vercel.sh;style-src 'self' 'unsafe-inline' vercel.com *.vercel.com vercel.live instant-preview-site.vercel.app instant-preview-site-bc8byjwcq.vercel.sh;font-src 'self' vercel.com *.vercel.com vercel.live instant-preview-site.vercel.app instant-preview-site-bc8byjwcq.vercel.sh *.gstatic.com;connect-src data: *;"
    },
    'content-type' => { name: 'Content-Type', value: 'text/html; charset=utf-8' },
    'date' => { name: 'Date', value: 'Sat, 16 Sep 2023 14:56:46 GMT' },
    'feature-policy' => {
      name: 'Feature-Policy',
      value: "fullscreen 'self'; camera 'none'"
    },
    'referrer-policy' => { name: 'Referrer-Policy', value: 'origin-when-cross-origin' },
    'server' => { name: 'Server', value: 'Vercel' },
    'strict-transport-security' => {
      name: 'Strict-Transport-Security',
      value: 'max-age=31536000; includeSubDomains; preload'
    },
    'vary' => {
      name: 'Vary',
      value: 'RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url'
    },
    'x-content-type-options' => { name: 'X-Content-Type-Options', value: 'nosniff' },
    'x-dns-prefetch-control' => { name: 'X-Dns-Prefetch-Control', value: 'on' },
    'x-download-options' => { name: 'X-Download-Options', value: 'noopen' },
    'x-edge-runtime' => { name: 'X-Edge-Runtime', value: '1' },
    'x-frame-options' => { name: 'X-Frame-Options', value: 'DENY' },
    'x-matched-path' => { name: 'X-Matched-Path', value: '/[[...slug]]' },
    'x-powered-by' => { name: 'X-Powered-By', value: 'Next.js' },
    'x-robots-tag' => { name: 'X-Robots-Tag', value: 'noindex' },
    'x-vercel-cache' => { name: 'X-Vercel-Cache', value: 'MISS' },
    'x-vercel-id' => {
      name: 'X-Vercel-Id',
      value: 'iad1:iad1:iad1::7zktw-1694876206302-13dbeb12bf20'
    },
    'x-xss-protection' => { name: 'X-Xss-Protection', value: '0' },
    'transfer-encoding' => { name: 'Transfer-Encoding', value: 'chunked' }
  },
  [Symbol(headers map sorted)]: null
}

'content-type' => { name: 'Content-Type', value: 'text/html; charset=utf-8' },

なので、やっぱり HTML が返ってきていますね。

コード修正

どうして HTMLが返ってきているのかは不明だったので、
レスポンスボディを取り出す処理を try-catch していきます

(詳しい方いたら、おしえてください!!)

import { config } from "@/lib/config";

// APIのURL
const url = config.apiPrefix + config.apiHost + "/api/user";
// APIへリクエスト
const res = await fetch(url, {
  cache: "no-store",
});

// レスポンスボディを取り出す
try {
  const data = await res.json();
} catch(error) {
  condsole.log(error)
}

try-catch ではなく、Client Component に変更するのもありだと思います

再デプロイ

修正したコードを GitHub に push して再デプロイしてみます。

成功しました!!

最後に

今回はとりあえず try-catch で逃げましたが、

API から HTML が返ってきている原因を探らないとですね。。

Discussion