500
アプリケーション自身が動いて、想定していなかった何かに当たって諦めた——という意味です。だから 502 や 504 と違って、どこかにスタックトレースが必ずあります。仕事はそれがどのログにいるかを見つけることです。
同じ 3 桁でも、リクエストのどちら側にいるかで別々の問題になります。自分に当てはまるブロックを読んでください。
これが出ている理由
サイト全体ではなく 1 つのエンドポイントで、しかも時刻ではなくペイロードと相関する傾向があります。特定のレコード、特定のファイルサイズ、特定の文字が現れるまでは同じページが動く、という形です。応答のボディはプロキシのものではなく、あなたの API の例外ハンドラかフレームワークのエラーページなので、`res.json()` はパースに成功してデータのように見えるエラーオブジェクトを渡してくるかもしれませんし、フレームワークが HTML まで落ちていれば `Unexpected token '<'` を投げるかもしれません。いずれにせよ、ブラウザが持っている情報は 3 桁の数字より多くありません。例外はサーバー側に留まったからです。
やること
エラーではなく、それを引き起こしたリクエストを捕まえてください。メソッド・URL・ヘッダー・ボディを正確に。バックエンドは再現できるリクエストを求めますし、その控えを持っているのはあなただけです。次に応答ヘッダーで相関 ID を読みます——`x-request-id`、`x-amzn-trace-id`、`cf-ray`——そしてその文字列を渡してください。対応するサーバーのログ行を数秒で見つけられる唯一の値です。パースの前に `res.ok` と `content-type` を確認して、紛れ込んだ HTML のエラーページが、本当の原因から 3 フレーム離れたパーサーのバグに化けないようにします。そして自動で再試行しないこと。500 は書き込みが起きたかどうかを知る手段を与えないので、再試行された注文は 2 件の注文になりえます。どうしても再試行可能にしたいなら、冪等キーを送ってサーバー側で重複排除させてください。
これが出ている理由
フレームワークの最終手段のハンドラが、どのハンドラも引き受けなかった何かを捕まえ、応答がプロセスを出る前に 3 桁の数字へ変えました。例外もスタックも問題の入力も全部ログの中にあり、応答の中には一つもありません。それが正しい姿です。漏らせば 500 は情報漏洩になります。系統は多くありません。特定の入力での未処理例外、答えなくなった依存先(枯渇したコネクションプール、リカバリ中のデータベース、期限切れの資格情報)、あるいは起動自体は問題なかったのに、特定のパスの最初のリクエストでしか読まない環境変数が欠けているプロセス、というあたりです。
やること
相関 ID から始めて、トレースの最後ではなく最初の例外を読んでください。最後のフレームはたいてい、本当のエラーの上で自分のエラーハンドラが落ちたものです。アプリケーションのログに何も無いなら、その 500 を書いたのはアプリケーションではありません。Apache のエラーログの「Premature end of script headers」は、ヘッダーより先に出力した CGI か suexec の権限拒否を指しますし、nginx は rewrite や内部リダイレクトの循環に対して自分で 500 を書きます。これは設定のバグであってコードのバグではありません。そのうえで、単純化したものではなく問題のペイロードそのもので再現してください。500 の引き金になる入力は、ほぼ常に誰も起こりうると思っていなかったほうです。入力が本当に不正だった場合は、修正を 4xx にしてください。バリデーションの失敗が 500 として届くのは、エラー処理が二重に間違っている状態です——落ちたことと、サーバーのせいにしたことと。最後に、1 件の 500 が 1 件の失敗リクエストだと決める前に `proxy_next_upstream` の設定を確認してください。nginx の `http_500` はオプトインの値なので、有効にしていると 1 件のクライアントリクエストがグループ内のすべての upstream に対して再生されえます。
これが出ている理由
あなたのしたことが原因ではありませんし、手元で何をしても変わりません。ウェブサイト自身のプログラムが、あなたのページを組み立てる途中で障害にぶつかりました。サイト全体ではなく 1 つの操作だけに出るのが普通で——検索は動くのに購入手続きだけ落ちる、という形です——フォームの珍しい文字や、サイトの想定より大きなファイルなど、リクエストの中の具体的な何かが引き金になっていることもあります。
やること
1 回だけリロードしてください。本当に一時的な障害なら勝手に直ります。直らないなら、有用なのは同じ操作を繰り返すのをやめることです。特に決済・予約・申し込みが絡んでいるなら。サーバーエラーは結果を隠しますが、作業を取り消してくれるとは限らないので、もう一度ボタンを押すと 2 回実行されることがあります。再試行の前に、注文履歴・受信トレイ・アカウントページを確認してください。報告するときは、正確な時刻、ページのアドレス、直前に入力した内容を添えてください。運営者はそこから対応するログを見つけられますし、入力したフィールドが引き金であることは非常によくあります。Cookie の削除、ブラウザの変更、ルーターの再起動は効きません。障害は接続の向こう側で起きています。
失敗した URL に対してそのまま実行してください。エラーページを表示せずにステータスだけを出すので、ブラウザが描画したものではなくサーバーが言ったことが見えます。
curl -sS -o /dev/null -D - -w '\n%{http_code} in %{time_total}s\n' -H 'content-type: application/json' --data '{"quantity":-1}' https://example.com/api/orders`-sS` は進捗表示だけを消してエラーメッセージは残し、`-o /dev/null` はボディを捨て、`-D -` は応答ヘッダーを標準出力に落とし、`--data` はペイロードを送ってリクエストを POST にし、`-w` はステータスと実時間を出します。まずヘッダーのブロックを読んでください。ステータス行には答えられない唯一の問い——これを書いたのはどの層か——に答えてくれます。`server:` にアプリケーションのフレームワーク名が出ている、あるいは自分のスタックが付けている `x-request-id` があるなら、リクエストはコードまで届いていて、例外はその ID でログの中にいます。`server: nginx` や `server: cloudflare` だけでアプリケーションのヘッダーが一つも無いなら、見ているのはアプリケーションの手前から返ったエラーページで、3 桁の数字を読み直す価値があります。使える答えを取れなかった中間装置が報告するのは 500 ではなく 502 だからです。2 つ目の手がかりは経過時間です。アプリケーション階層の 500 はたいてい成功と同じ速さで返ります。例外は待った末ではなく投げられたものだからです。だから 30 秒かかる 500 はコードの中のタイムアウトで、投げた行ではなく固まった依存先を指しています。正常だと分かっているペイロードに差し替えてエンドポイント自体が動くことを確かめ、失敗するほうは取っておいてください。それが再現手順であり、バックエンドがログからは復元できないものです。
RFC 9110 §15.6.1 は 500 を、サーバーが予期しない状況に遭遇してリクエストを果たせなかった状態と定義しています。その外側の 5xx クラスを §15.6 は「サーバーがリクエストを実行できない」と紹介しています。この定義は仕様の中でわざといちばん具体性が低く、そこが役に立つところです。500 は診断ではなく自白なので、コード自体は何が壊れたかを一切運びません。運ぶのは「誰が気づいたか」だけです。ここから 3 つの帰結が出ます。1 つ目、アプリケーションが出した 500 は完全に正当な HTTP 応答なので、手前のプロキシは自前の応答に差し替えず、そのまま中継します。RFC 9110 §15.6.3 が、中間装置が不正な応答を受け取った場合のために 502 を取ってあるのはこのためですし、500 は「コードが走った証拠」で、502 はしばしば「走ってすらいない」という意味になるのもこのためです。2 つ目、500 は RFC 9110 §15.1 のヒューリスティックにキャッシュ可能なステータスコードの一覧——200・203・204・206・300・301・308・404・405・410・414・501、それだけです——に入っていません。だから中間装置が独断で保存することはなく、居座り続ける 500 はキャッシュから配られているのではなく、リクエストのたびに作り直されています。3 つ目、500 はそのリクエストに効果があったかどうかについて何も言いません。予期しない状況に当たるまでにサーバーがどこまで進んだのかを知る手段を、仕様はクライアントに与えていません。決済や注文の 500 が自動的に再試行して安全とは言えない理由がこれです。サーバーはアプリケーションのコードだけでなく、自分自身の機構からも 500 を出します。Apache の CGI のドキュメントは、エラーログに「Premature end of script headers」と出る「Internal Server Error」を、スクリプトが HTTP ヘッダーより先に何かを出力したか、suexec の権限チェックが拒否したかだと説明していますし、nginx は rewrite や内部リダイレクトの循環といった自分の内部障害に 500 を返します。どちらの場合も 3 桁の数字は同じで、内容のすべてはログの 1 行にあります。
| 紛らわしいコード | 見分け方 |
|---|---|
| 502 | 500 はアプリケーションが自分の失敗を認めたもの、502 は中間装置が「アプリケーションから使える答えを受け取れなかった」と報告したものです。RFC 9110 §15.6.3 は 502 を、内側のサーバーから受け取った不正な応答と定義していて、アプリケーション自身の 500 は正当な応答なので、プロキシはそのまま素通しします。実務的には、500 なら探すべきスタックトレースが存在し、502 はしばしばハンドラが走る前にワーカーが死んでいて、アプリケーションのログが空です。 |
| 503 | 503 は判断で、500 は事故です。RFC 9110 §15.6.4 は 503 を、過負荷や計画されたメンテナンスのためにサーバーが一時的にリクエストを扱えない状態と定義し、どれだけ待てばいいかを示す `Retry-After` を載せられるようにしています。500 のほうには「待て」を示唆するものが何もありません。通る見込みが何も無いからです。負荷を捨てるのに例外を投げているサービスは、計画された拒否をクラッシュとして報告していることになり、503 をキーにしたクライアントのバックオフはすべて空振りします。 |
| 400 | RFC 9110 §15.5.1 は 400 を、クライアント側の誤りと受け取れる何かのせいでサーバーがリクエストを処理できない状態と定義しています。エンドポイントが拒む入力に対しては、これが正直なコードです。入力の検証中に上がる 500 は、サーバーではなくエラー処理のバグです。答えられるリクエストで、答えは「駄目」だったのに、5xx はクライアントに「絶対に成功しないもの」の再試行を促してしまいます。 |
Cloudflare 520 | Cloudflare 越しならこの 2 つは簡単に見分けられますし、知っておく価値があります。Cloudflare は 520 を、オリジンが空・不明・予期しない応答を返した場合——128 KB の上限を超えるヘッダーも含みます——と説明しています。つまり 520 は、オリジンの答えが使える HTTP ではなかったという意味です。Cloudflare 越しに届く 500 はその逆で、アプリケーションが意図的に生成した整った応答を、Cloudflare がそのまま中継したものです。 |
501 | RFC 9110 §15.6.2 は 501 を、リクエストを果たすのに必要な機能をサーバーがサポートしていない状態と定義し、メソッドを認識できない場合の適切な答えとして名指ししています。500 と違って、1 件のリクエストがうまくいかなかったという話ではなくサーバーの能力についての表明ですし、RFC 9110 §15.1 がヒューリスティックにキャッシュ可能と挙げる数少ないエラーコードの一つでもあります。だから誤った 501 はキャッシュに保存され、原因が消えたあとも繰り返されえます。500 にはそれが起きません。 |
どれもブラウザ内で完結します。アップロードは発生しません。
500 を調べているとき、ついでに読むことになりがちなコードです。
ページではなく応答ヘッダーを読んでください。curl -sS -o /dev/null -D - https://example.com/path が本文なしでヘッダーだけを出し、server・via・cf-ray の 3 つで、答えたのがどの層かが分かります。cf-ray の値があれば応答を扱ったのは Cloudflare で、その値はサポートが聞いてくる ID です。エッジを完全に外して確かめたいなら、--resolve example.com:443:203.0.113.10 を付けて同じリクエストを投げ直します。元のホスト名と SNI を送りつつ、指定したオリジンのアドレスへ接続するので、答えが変われば、エッジとオリジンの言い分が食い違っているということです。
ありません。分岐条件にしてはいけません。RFC 9112 はクライアントに理由句を無視するよう求めていますし、サーバーは自由に変えられますし、HTTP/2 と HTTP/3 にはそもそも理由句が存在しません。HTTP/1.1 では 404 Not Found として届くものが、HTTP/2 では素の 404 として届きます。このサイトが登録済みの理由句を載せているのは、それが検索される語であり HTTP/1.1 のログに出る文字列だからで、何かのソフトウェアがそれに依存しているからではありません。
5xx と 429 は再試行してよく、4xx はしないでください。次に投げても中身は同じだからです。応答に Retry-After が付いていればそれに従ってください。RFC 9110 がまさにこのために定義していて、値は秒数でも日時でも構いません。付いていなければ、ジッター付きの指数バックオフと明確な上限を使い、しかも冪等なメソッドに限ってください。再試行された POST はカードに二重で課金しかねません。すでに失敗しているサーバーへの再試行の嵐は、短い障害を長い障害に変える典型的な経路です。
変えられます。守るべき規則は 1 つだけ、そのコードが本当であること。応答を自動で読むもの——検索エンジン、監視、キャッシュ、クライアント側の再試行ロジック——はすべて数字だけで判断し、ページの中身は一切見ません。エラーページを 200 で返せば自分の監視から障害が隠れますし、無いページを 200 で返せばエラーが本文として索引されますし、単に不正なリクエストに 500 を返せば、当番の人をスタックの間違った半分へ送り込むことになります。
500 でまだ詰まっているなら、ステータスコード一覧をすべて見る。あるいは上に戻って、自分の立ち位置に向けて書かれたブロックを読んでください。