ViteでUncaught (in promise) SyntaxError: Unexpected token '<', "<html lang"... is not valid JSONが出たらappTypeを変更しろ

## 先に結論だけ

Viteでエラー`Uncaught (in promise) SyntaxError: Unexpected token '<', "<html lang"... is not valid JSON`が発生したら、`vite.config.ts`の[appType](https://ja.vite.dev/config/shared-options.html#apptype)オプションを`mpa`に変更してください。Viteの開発サーバーが404エラーを返すようになります。どのパスに問題があるのかブラウザの開発者ツールから調査でき、対処が簡単になります。

## はじめに

この記事は、[Vite](https://ja.vite.dev/)におけるfetchに関連するエラー、およびViteサーバーのミドルウェアについて解説するものです。

### 対象とする読者

- HTML / CSS / JavaScriptの基礎的な知識がある
- Node.jsの基礎的な知識がある
- Viteを使用したことがあるが、学習初期である

:::note warn
この記事は、それぞれのツールのインストール方法などは解説しません。あらかじめご了承ください。
:::

### 対象とする環境

- Vite v5.4.10
- Google Chrome v130.0.6723.92

:::note warn
この記事は、Vite v5.4での挙動について記載しています。将来のバージョンでは挙動が変わるかもしれないので、記事を読む前にお手元の環境のバージョンを確認してください。
:::

## 問題の詳細

この記事で取り扱う問題の詳細と再現方法を説明します。

### 再現方法

最初に、Viteプロジェクトの[publicディレクトリ](https://ja.vite.dev/guide/assets#the-public-directory)にJSONファイルを配置します。次にindex.htmlからJSONファイルをfetchします。

```js
const getJson = async () => {
    const response = await fetch("./wrong_path.json");
    const responseJson = await response.json();
}
```

その際、ファイルパスが誤っていると以下のエラーが発生します。

```console
Uncaught (in promise) SyntaxError: Unexpected token '<', "<html lang"... is not valid JSON
```

パスが誤っているため、404エラーが返ってくると考えてしまいますが、実際にはJSONのパースエラーが発生します。

### レスポンスの確認

ブラウザーの開発者ツールを使って、レスポンスを確認します。

![スクリーンショット 2024-11-06 16.45.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/9556/bd01d8c9-a7df-76d7-0f1f-94761cdb8c8d.png)

開発者ツールのネットワークタブを開き、各ファイルのレスポンスを確認します。すると`./wrong_path.json`へのリクエストに対してなぜか`index.html`の内容が返されています。

この問題は

- 誤ったパスでJSONファイルをリクエストする
- Viteサーバーが404エラーを返さず、`index.html`を返す
- JavaScriptが`index.html`をJSONとしてパースしようとしてエラーが発生する

という流れで発生したことがわかりました。`Unexpected token '<', "<html lang"`というエラーは、htmlファイルの先頭にある`<html`タグをJSONとして解釈しようとし、失敗したこと示しています。

## なぜViteは404エラーを返さないのか

Viteサーバーはなぜ404エラーを返さないのでしょうか？ソースコードから調査してみます。

### SPA履歴フォールバックミドルウェア

Viteサーバーは本番環境を模倣するため、ミドルウェアによって挙動を変更できます。デフォルトで導入されるミドルウェアの1つに[SPA履歴フォールバックミドルウェア](https://github.com/vitejs/vite/blob/936f3b324d1ce257836ddd950651034ceb30db41/packages/vite/src/node/server/middlewares/spaFallback.ts)があります。このミドルウェアは、シングルページアプリケーションを配信することを念頭に、リクエストされたファイルが存在しない場合、`index.html`を返します。

Viteのデフォルト設定では、SPA履歴フォールバックミドルウェアが[有効になっています](https://github.com/vitejs/vite/blob/936f3b324d1ce257836ddd950651034ceb30db41/packages/vite/src/node/server/index.ts#L491)。

### 静的アセットはimportする

Viteは配信するアセットを最適化するため、[import](https://ja.vite.dev/guide/assets.html)することを推奨しています。開発環境、本番環境を問わず、アセットをimportすることでViteが最適化を行い、開発者は実際のアセットがどこに配置されているかを気にする必要がありません。

importパスが誤っている場合、Viteはビルド時にエラーを出力します。ビルドされたサイトには誤ったパスが含まれないことが保証されます。

### Viteの設計思想

- Viteはデフォルトでシングルページアプリケーションを配信するように設定されている
- 静的アセットはimportされているため、ファイルパスが誤っていることは想定されていない

この2つの理由から、Viteサーバーはデフォルトでは404エラーを返さず、`index.html`を返します。

## 解決策

なぜViteが404エラーを返さないのか、その理由を理解したところで、解決策を考えていきます。

### 404エラーを返す方法

Viteサーバーが404エラーを返すようにするには、`vite.config.ts`の[appType](https://ja.vite.dev/config/shared-options.html#apptype)オプションを`mpa`に変更します。

```ts
// vite.config.ts

import { defineConfig } from "vite";

export default defineConfig({
  ...
  appType: "mpa",
  ...
});
```

この設定はマルチページアプリケーションを配信するためのものです。その結果SPA履歴フォールバックミドルウェアが無効になり、Viteサーバーが404エラーを返すようになります。ブラウザーの開発者ツールやコンソールでエラーを確認し、問題のあるパスを特定できます。

### サーバー内に配置するアセットは明示的URLでインポートする

Viteには[明示的なURLのインポート](https://ja.vite.dev/guide/assets#explicit-url-imports)という機能があります。この機能では、アセットパスがViteによって変更されず、ファイル名にハッシュ値が追加されたり、インライン化されないことを保証します。この機能は本来rollup.jsでバンドルできないWebAssemblyファイルを扱うためのものですが、publicフォルダー内のファイルをインポートする際にも利用できます。

```js   
import jsonURL from "/correctPath.json?url";

const getJson = async () => {
    const response = await fetch(jsonURL);
    const responseJson = await response.json();
}
```

URLインポートはビルド時に評価されるため、ファイルパスが誤っているとエラーが発生します。これによりクライアントエラーを待たず開発時にエラーを検知できます。

#### publicディレクトリのURLは絶対パスで指定する

publicディレクトリのファイルは[絶対パスで指定](https://ja.vite.dev/guide/assets#the-public-directory)しなければいけません。相対パスで指定すると、Viteはファイルパスを解決できずにビルド時にエラーを出力します。

```js
import jsonURL from "/correctPath.json?url"; // 👌 OK "/"で始まる絶対パス
import jsonURL from "./correctPath.json?url"; // 🚫 NG "./"で始まる相対パス
```

## 個人的な感想

Viteは最低限の設定で一般的な開発パターンを実現することを[理念](https://ja.vite.dev/guide/philosophy.html)としています。設計を理解していくと使いやすいツールですが、学習初期は戸惑うこともあります。

個人的にViteは今後も発展すると思うので、学習する価値があります。

## 参考記事

https://stackoverflow.com/questions/69701743/how-can-i-configure-vites-dev-server-to-give-404-errors