NginxでのWebSocket 400エラーの解決:実用的なリバースプロキシ設定ガイド

intermediate🌐 Networking2026-06-18| Ubuntu/Debian/CentOS, Nginx 1.10以上, バックエンド: Node.js (Socket.io), Go, または Python (FastAPI/Django Channels)

Error Message

WebSocket connection to 'wss://example.com/ws' failed: Error during WebSocket handshake: Unexpected response code: 400
#websocket#nginx#プロキシ#wss#socket.io

問題の発生

ローカル環境では完璧に動作していても、Nginxリバースプロキシの背後にデプロイした途端、リアルタイム機能が動かなくなることがあります。スムーズなハンドシェイクの代わりに、ブラウザのコンソールには次のような不満の残るエラーが表示されます。

WebSocket connection to 'wss://example.com/ws' failed: Error during WebSocket handshake: Unexpected response code: 400

焦る必要はありません。原因の多くはNginxのデフォルトの動作にあります。Nginxはすべてのリクエストを標準のHTTPトラフィックとして扱います。その過程で、バックエンドが永続的な接続を維持するために必要な特定のヘッダーを削除してしまうのです。

クイック解決策

Nginxに対して、UpgradeConnectionヘッダーを直接バックエンドに渡すように指示する必要があります。サイトの設定ファイル(通常は/etc/nginx/sites-available/にあります)を開き、locationブロックを更新してください。

location /ws {
    proxy_pass http://localhost:3000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;

    # 60秒のデフォルトタイムアウトによってアイドル接続が切断されるのを防ぐ
    proxy_read_timeout 86400;
}

構文をテストし、サービスをリロードして変更を適用します。

sudo nginx -t
sudo systemctl reload nginx

原因(技術的な解説)

WebSocketは特殊です。最初は標準的なHTTPリクエストとして始まり、Upgrade: websocketConnection: Upgradeという2つの特定のヘッダーを使用して、永続的なプロトコルへと「アップグレード」されます。

NginxはHTTP/1.1仕様に厳密に従っています。これらを「ホップバイホップ(hop-by-hop)」ヘッダーとして扱うため、明示的に指示しない限り、クライアントからプロキシ先のサーバーへは渡されません。Node.jsやGoのバックエンドがこれらのヘッダーのないWebSocketリクエストを受信すると、不正なリクエストであると判断します。これが、発生しているHTTP 400 Bad Requestの結果です。

根本原因の特定

400エラーはプロトコルの不一致を示すシグナルです。バックエンドのログに接続試行が記録され、すぐに消えてしまう場合、Nginxがアプリケーションコードに届く前にハンドシェイクヘッダーを遮断している可能性が高いです。

本番環境向けの高度な設定

特定のlocationブロックに「upgrade」をハードコードするのは、単発の修正には適しています。しかし、サーバーが標準のHTTPとWebSocketの両方を処理する場合、動的なマッピングを使用する方がはるかにスマートです。これにより、標準のリクエストはkeep-alive状態を維持し、WebSocketは必要なアップグレードを受けることができます。

/etc/nginx/nginx.confhttpブロック内にこのマッピングを追加します。

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

これで、すべてのサイトでより柔軟な設定を使用できるようになります。

location /ws {
    proxy_pass http://backend_cluster;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
}

確認手順

修正が正しく適用されたことを確認するために、以下の3つのポイントをチェックしてください。

- **ネットワークタブの確認:** デベロッパーツール(F12)を開き、"WS"でフィルタリングします。`/ws`リクエストのステータスが`101 Switching Protocols`になっているはずです。
- **ハンドシェイクヘッダーのチェック:** `Request Headers`と`Response Headers`の両方に`Upgrade: websocket`が含まれていることを確認します。
- **CLIでのテスト:** `wscat`を使用してブラウザを介さずにテストします:`wscat -c wss://example.com/ws`。

セキュリティと信頼性に関するヒント

私は、デプロイ中のわずかなファイルの破損が「ゴーストバグ(原因不明のバグ)」を引き起こすことを痛いほど経験してきました。Nginxの変更を本番環境に反映する際は、まずファイルの整合性を確認します。これにより、発生するはずのない設定の乖離をデバッグする時間を大幅に節約できます。

私は、ToolCraftのハッシュジェネレーターを使用して、ローカルで.confファイルのSHA-256チェックサムを作成しています。すべてブラウザ内で処理されるため、ハッシュを取得するためだけに機密性の高いサーバーロジックをクラウドにアップロードする必要がありません。

タイムアウトに関する注意

Nginxのデフォルトのproxy_read_timeoutは60秒です。ユーザーから「チャットやダッシュボードが1分おきに切断される」という苦情がある場合、これが原因です。この値を86400(24時間)に引き上げることでパイプラインを開いたままにできますが、アプリ側で基本的なping/pongによるハートビートを実装し、実際に切断された接続を整理することを忘れないでください。

重要なポイント

- WebSocketには**HTTP/1.1**が必要です。1.0では動作しません。
- Nginxは厳格なゲートキーパーであり、デフォルトでホップバイホップヘッダーを削除します。
- 400エラーは、バックエンド側が「これはWebSocketリクエストとして認識できない」と言っているサインです。

Related Error Notes