🔔

無限ローディングに泣かない!CORSとポート競合の向き合い方

に公開

はじめに

Webアプリケーションを開発していると、デプロイ環境では問題なく動くのに、なぜかローカル環境だけは無限ローディングのままでデータが表示されない、そんな経験ありませんか?

最近、私自身がまさにこの無限ローディング地獄に陥ってしまいました。GitHub Actionsでデプロイされたプレビュー版は問題なく動きます。しかし、最新にしたはずの自分のローカル環境だと、テーブルがずっとくるくる回りっぱなし。

ブラウザの開発者ツールを見ても、明確なエラーが分からず頭を抱えました。

Web開発のセキュリティ「CORS」とは

無限ローディングの裏に潜んでいた原因、それがCORS(Cross-Origin Resource Sharing)でした。

CORSは、Webセキュリティのためにブラウザに実装されている重要な仕組みです。
Webページがとある場所から、別の場所にあるサーバーにデータを取りに行こうとする際に、ブラウザが「本当にアクセスしていいの?」と確認するためのルールだと思ってください。

この「場所」のことを、「オリジン」と呼びます。オリジンは、以下の3つの要素で構成されます。

プロトコル: http または https
ドメイン: example.comlocalhost
ポート: 80, 443, 5173, 8787 など

例えば、http://localhost:5173http://localhost:8787 は、ドメインは同じlocalhostですが、ポートが異なるため、ブラウザからは「別のオリジン」と見なされます。

フロントエンドとバックエンドのオリジンが異なる場合、ブラウザはセキュリティのためにAPIリクエストを自動的にブロックします。これがCORSエラーです。バックエンド側で、どのオリジンからのアクセスを許可するかを明示的に設定してあげなければなりません。

https://developer.mozilla.org/ja/docs/Web/HTTP/Guides/CORS

CORS設定の落とし穴とポート競合の沼へ

バックエンドのCORS設定を見ると、http://localhost:5173からのリクエストだけを許可する設定になっているのを発見しました。

origin: (origin) => {
        if (
          origin?.endsWith("hoge") ||
          origin?.endsWith("hoge") ||
          origin === "http://localhost:5173" ←該当箇所
        ) {
          return origin;
        }
        return null;

これは、バックエンドが、特定のポート(このコードでは5173)からのアクセスしか許可していないことを意味します。

しかし、私のローカル環境でフロントエンドとバックエンドを起動すると、どういうわけかポート番号がバラバラになっていました。

フロントエンドの起動ログ: Local: http://localhost:5174/http://localhost:5176/
バックエンドのCORS設定で許可されている5173ではないポートで立ち上がっていました。
バックエンドの起動ログ: http://localhost:65364/
本来http://localhost:8787で起動するはずなのに、毎回ランダムなポートで起動していました。

つまり、バックエンドのCORS設定は特定のポート(5173)からのリクエストしか許可していないのに、フロントエンドは別のポート(51745176)で、バックエンドは毎回異なるランダムなポートで起動している状態になっていました。その結果、ブラウザはCORSルールに従って、知らないオリジンからの通信をブロックしていました。バックエンドからのデータ応答がフロントエンドに渡らなかったため、フロントエンドのデータ読み込みが完了していませんでした。そして、私のフロントエンドの実装が、このデータが届かない状況に対して適切にタイムアウトやエラーハンドリングを行わず、無限にローディングし続ける仕様になっていたため、無限ローディングが発生していたのです。

ではなぜ、51738787といった本来使いたいポートで起動できず、ランダムなポートになってしまうのか?その原因はポート競合にありました。

過去にフロントエンドやバックエンドを起動した際に、プロセスが正常に終了せず、裏でポートを占有し続けていたのです。例えば、5173ポートが既に別のアプリケーションに占有されていると、次にフロントエンドを起動しようとしても5173を使えず、自動的に空いている51745176に割り振られてしまいます。バックエンドも同様に、8787が占有されていると、ランダムな別のポートを探してしまうのです。

無限ローディング地獄からの脱出方法!具体的なコマンドとステップ

ここからが、私が実際に一つずつ問題を解決していった具体的な手順です。

1.ポートを占拠しているプロセスを特定し、強制終了させる
まず、どのプロセスがポートを占有しているのかを突き止めるコマンドを使いました。

  1. ポートの利用状況を確認する: lsof -i :[ポート番号]
    ターミナルで、使いたいポート(例:51738787)を指定してlsofコマンドを実行します。
lsof -i :5173 # 例: フロントエンドが使いたいはずのポート
lsof -i :8787 # 例: バックエンドが使いたいはずのポート

私の場合は、lsof -i :5173を実行すると、nodeプロセス(PID 4309)がポート5173を使っていることが分かりました。

COMMAND    PID         USER   FD   TYPE       DEVICE SIZE/OFF NODE NAME
node      4309 your_user   10u  IPv4 0xdeadbeef    0t0  TCP localhost:5173 (LISTEN)
  1. 占拠しているプロセスを強制終了する: kill [PID]
    lsofで特定したPID(プロセスID)を使って、そのプロセスを終了させます。
kill 4309 # 例としてPID 4309を終了

これを実行すると、ポートが解放されます。念のため、再度lsof -i :[ポート番号]を実行して、何も表示されないことを確認しました。

2.環境変数の確認とサーバーの再起動
ポートを解放した後に、フロントエンドがバックエンドの正しいURLにアクセスしているかを確認し、両方のサーバーを再起動しました。

  1. バックエンドの現在の起動ポートを確認する
    現在バックエンドの開発サーバーが起動しているターミナルで、サーバーが待ち受け状態になったことを示すログ(例: Ready on http://localhost:XXXXX のような表示)の XXXXX の部分を確認しました。これが、バックエンドが今まさに起動しているポートです。

  2. フロントエンドの環境変数を確認する
    フロントエンドの環境変数を定義するファイルを開き、バックエンドのURLを参照している変数の値が、上記で確認したバックエンドの現在のポートと一致しているか確認しました。

  3. 両サーバーの再起動
    すべての設定変更が反映されるよう、バックエンドとフロントエンドの開発サーバーを Ctrl + C で停止し、改めて起動し直しました。

その他の解決策や考慮すべき設定

上記の手順で私の無限ローディング問題は解決しました。しかし、Web開発におけるCORSやポートの問題には、他にも様々な解決策や考慮すべき設定が存在します。

1.バックエンドのポートを固定する設定を導入する
もしバックエンドが毎回ランダムなポートで起動してしまう場合、開発環境の設定ファイルでポートを固定することを検討してみてください。

  • wrangler.toml (Cloudflare Workers) の例:
    wrangler.tomlファイルに、開発サーバー用の設定セクション([dev])を設け、portを指定します。
# wrangler.toml の [dev] セクションに追記
[dev]
port = 8787  # 例: 8787に固定
  • Node.js/Express など他のバックエンドの場合:
    通常、app.listen(PORT)PORT 変数を.envファイルや設定ファイルで固定できます。

ポートが固定されれば、フロントエンドの環境変数も常にその固定されたポートを指すように設定でき、起動ごとのポート番号の確認・修正の手間がなくなります。

2.フロントエンドの開発サーバーでポートを優先して起動させる設定
フロントエンドの開発サーバー(例: Vite)も、特定のポートを優先して起動するように設定できます。これにより、もしそのポートが空いていれば、必ずそのポートで起動するようになります。

  • vite.config.ts (Vite/SvelteKit) の例:
    vite.config.tsファイルに、serverオプションを追加し、portstrictPortを指定します。
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';

export default defineConfig({
    plugins: [sveltekit()],
    server: {
        port: 5173, // フロントエンドが5173ポートを使うように明示的に指定
        strictPort: true // 5173が使われていたらエラーにする(ポートを動的に変更しない)
    }
});

3.バックエンドのCORS設定を調整する
もしポートを固定するのが難しい場合や、複数の開発環境からアクセスする可能性がある場合は、バックエンドのCORS設定で複数のオリジンを許可することも検討できます。

cors({
  origin: (origin: string, c) => {
    if (
      origin === "http://localhost:5173"  // 許可したいポート1
      origin === "http://localhost:5174"  // 許可したいポート2
      origin === "http://localhost:5176"  // 許可したいポート3
    ) {
      return origin;
    }
    return null;
  },
});

まとめ

今回の無限ローディング地獄は、私の知識不足と環境固有の挙動が絡み合った、非常に複雑なものでした。しかし、この経験を通じて以下のことを学べました。

  • Web開発におけるCORSの重要性と、そのエラーがブラウザ側でどのように発生するか。

  • ローカル開発環境でのポート競合が、いかに厄介な問題を引き起こすか。

  • 開発サーバーの設定ファイルを通じて、環境を正確に制御することの重要性。

  • そして何より、「デプロイ環境で動くのにローカルで動かない」という時は、コードではなく環境設定やツールの挙動に原因がある可能性が高いということ。

この経験が、同じような問題に直面する皆さんの助けになれば幸いです。

参考になる記事

今回の内容に関連するZennやその他の記事をいくつかご紹介します。

Emoba Tech Blog

Discussion