エラーの概要
突然502 Bad Gatewayが発生し、ユーザーが弾かれている。Nginxのエラーログにはこのようなメッセージがあるはずです:
2024/01/15 10:23:41 [error] 1234#1234: *1 upstream sent too big header while reading response header from upstream,
client: 192.168.1.100, server: example.com, request: "GET /dashboard HTTP/1.1",
upstream: "http://127.0.0.1:3000/dashboard", host: "example.com"
パターンは一度気づけば明らかです。ログイン直後や、JWTトークンとセッションクッキーを設定するページで502が発生します。匿名ページは問題なく読み込まれます。
根本原因
Nginxはアップストリームからのレスポンスヘッダーを固定サイズのバッファに読み込んでからクライアントに転送します。ヘッダーが大きすぎると、Nginxはレスポンス全体を拒否し、アップストリームはボディを送信できなくなります。
デフォルトのproxy_buffer_sizeは4KBです。一見十分に思えますが、現代のアプリが実際に送信するものを見ると話が変わります。JWTトークン1つだけで1〜3KBになります。Railsのセッションクッキーでさらに1〜2KB追加されます。OAuthフローのSet-Cookieヘッダーが数個加われば、すでに制限を超えてしまいます。RailsやDjangoアプリの典型的な認証済みレスポンスでは、ヘッダーだけで6〜10KBに達することがあります。
proxy_buffersディレクティブはレスポンスボディのバッファリングに使われるプール全体を制御します。両方の設定を十分な大きさにする必要がありますが、まず問題になるのはproxy_buffer_sizeです。
ステップ1 — 原因を確認する
設定を変更する前に、アップストリームが実際に大きすぎるヘッダーを送信しているか確認します。Nginxをバイパスしてアプリに直接クエリを送ります:
# アップストリームに直接アクセス — Nginxを完全にスキップ
curl -si http://127.0.0.1:3000/dashboard | head -50
# ヘッダーの合計サイズをバイト単位で計測
curl -si http://127.0.0.1:3000/dashboard \
| awk '/^\r?$/{exit} {total += length($0) + 2} END{print "Header bytes:", total}'
4KBを超えていますか?それが原因です。よくある問題の要因:
Set-Cookieに格納されたJWTトークン — 1つで簡単に1〜3KBになる- RailsやDjangoセッションからの複数の認証クッキー
- レスポンスヘッダーとして渡されるOAuthトークンやSAMLアサーション
- 古いクッキーを削除せずに積み重ねるアプリ
ステップ2 — プロキシバッファサイズを増やす
該当するサーバーブロック(通常は/etc/nginx/sites-available/または/etc/nginx/conf.d/)を開き、locationブロック内にバッファディレクティブを追加します:
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://127.0.0.1:3000;
# デフォルトは4k — ヘッダーサイズに応じて16kまたは32kに増やす
proxy_buffer_size 16k;
# レスポンスボディ用のバッファプール合計
proxy_buffers 8 16k;
# proxy_buffer_sizeの約2倍に設定する
proxy_busy_buffers_size 32k;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
適切なサイズの選び方:
16kはJWTとセッションクッキーの大多数のケースをカバーする- ヘッダーが常に大きい場合(複数の大きなトークン)は
32kに上げる proxy_busy_buffers_sizeはproxy_buffer_sizeの約2倍に設定する- 値はシステムのメモリページサイズ(Linuxでは通常4KB)の倍数でなければならない
ステップ3 — 設定を適用する
リロードする前に必ずテストしてください。構文エラーがあると、そのNginxインスタンス上のすべてのサイトがダウンします。
# まずテスト — このステップは絶対にスキップしないこと
nginx -t
# 期待される出力:
# nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
# nginx: configuration file /etc/nginx/nginx.conf test is successful
# グレースフルリロード — アクティブな接続は切断されない
nginx -s reload
# または
systemctl reload nginx
ステップ4 — 修正を確認する
# 502を返していたエンドポイントをテスト
curl -si https://example.com/dashboard -o /dev/null -w "%{http_code}\n"
# 200が返ってくるはず
# エラーログを監視 — メッセージが消えたことを確認
tail -f /var/log/nginx/error.log
別の方法:httpブロックでグローバルに適用する
複数のロケーションや仮想ホストで問題が発生している場合は、/etc/nginx/nginx.conf内のhttpブロックにバッファを一度設定します:
http {
# ...
proxy_buffer_size 16k;
proxy_buffers 8 16k;
proxy_busy_buffers_size 32k;
# ...
}
locationレベルのディレクティブは常にhttpレベルのものを上書きするため、必要に応じてルートごとの調整も引き続き機能します。
補足:fastcgi_buffer_size(PHP-FPM)の確認
proxy_passの代わりにfastcgi_passを使用していますか?エラーメッセージは同じですが、修正には別のディレクティブを使います:
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
fastcgi_buffer_size 16k;
fastcgi_buffers 8 16k;
fastcgi_busy_buffers_size 32k;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
根本的な問題は同じで、ディレクティブのプレフィックスが異なるだけです。proxy_*に設定するのと同じバッファサイズを使用してください。
再発防止
- **JWTをクッキーに格納しない:**このエラーの最も一般的な原因です。クッキーには短いセッションIDだけを保持し、実際のトークンはサーバーサイドに格納しましょう。ヘッダーサイズが劇的に減少します。
- **クッキーを適切に期限切れにする:**古いクッキーを削除せず追加し続けるフレームワークは、どれだけバッファを大きくしても最終的にオーバーフローします。クッキーのライフサイクルを見直してください。
- **大きなトークンを分割する:**大きなトークンが避けられない場合、一部のフレームワーク(NextAuthなど)は複数の小さなクッキーに自動的に分割できます。
- **最初からデフォルト値を上げておく:**リリース前にベースのNginx設定に
proxy_buffer_size 16kを追加しましょう。4KBのデフォルト値は別の時代のものです。現代の認証ヘビーなアプリは必ずいつかこの上限に達します。 - **ステージングでヘッダーサイズを確認する:**認証済みエンドポイントに対して
curl -siを実行し、数値を確認してください。ステージングで8KBのヘッダーを発見する方が、本番環境での深夜インシデントよりはるかにましです。

