ByteScope

302

HTTP 302 Found の直し方

3xx リダイレクトIETF 標準RFC 9110

一時的な迂回です。リソース自体は要求した URL のままそこにあるけれど、今回だけは別の場所から返したい——という意味で、しかもブラウザはその迂回をあなたのコードに見せないまま辿ります。

302 が出ている理由と、やるべきこと

同じ 3 桁でも、リクエストのどちら側にいるかで別々の問題になります。自分に当てはまるブロックを読んでください。

curl で 302 を再現する

失敗した URL に対してそのまま実行してください。エラーページを表示せずにステータスだけを出すので、ブラウザが描画したものではなくサーバーが言ったことが見えます。

curl
curl -sS -D - -o /dev/null -L -d 'user=me' -w '\nfinal %{http_code} after %{num_redirects} hops at %{url_effective}\n' https://example.com/login

`-d` で本物のリクエストボディを送り(これだけで POST になります。`-X` は故意に付けません——curl はそれを連鎖中のすべてのリクエストに適用するので、ここで見たいメソッドの書き換えそのものを潰してしまいます)、`-L` で連鎖を辿り、`-D -` で全ホップのヘッダーを落とし、`-w` で最終ステータス・ホップ数・答えた URL を出します。このダンプの中で次の一手を決めるのは 4 か所です。最初のステータス行が 302 と 303 と 307 を分けます。「メソッドが変わりうる」「次は GET と規定されている」「そのまま再送する」の違いです。`location:` の値は、アプリケーションが正しいリクエスト像から URL を組み立てたかを見せます。HTTPS のサイトなのに `http://`、内部ホスト名、コンテナの中にしか存在しないポート——どれも `X-Forwarded-Proto` か `X-Forwarded-Host` の欠落を指しています。同じ応答の `set-cookie:` は、ログインループの判定が下る場所です。平文 HTTP 上の `Secure` Cookie、`Secure` なしの `SameSite=None`、宛先と共有していない `Domain` は、次のホップがセッションなしで着いてまた跳ね返ることを意味します。そして最終応答の `content-type` が、フロントエンドからは見えなかった決定打です。JSON を頼んだのに `text/html` なら、それがログイン跳ね返しです。curl は 301・302・303 を辿るときは POST を GET に落とし、それ以外の 3xx ではメソッドを保つと明記しているので、`--post302` を付けて実行し直してサーバーのログを見比べれば、フィールドを落としたのがメソッドの書き換えかどうかを証明できます。

302 が実際に意味していること

RFC 9110 §15.4.3 は 302 を、対象リソースが一時的に別の URI にある状態と定義し、301 と分ける決定的な一文を添えています。リダイレクト先は折に触れて変わりうるので、クライアントは今後のリクエストでも元の URI を使い続けるべきだ、というものです。同じ節には歴史的な但し書きもあります。ユーザーエージェントは POST を GET に変えてもよく、それが困る場合は 307 (Temporary Redirect) を使え、と。書いていないことのほうも同じくらい役に立ちます。302 は RFC 9110 §15.1 がヒューリスティックにキャッシュ可能とするステータスコードの一覧に入っていません。つまり `Cache-Control` を明示しない限り 302 は保存されず、毎回あらためて問い合わせが来ます。試行錯誤に向いたリダイレクトなのはこの性質のおかげです。ログインまわりを書く前に、もう一つだけ整理しておく価値があります。§15.4.4 は 303 (See Other) を「サーバーがユーザーエージェントを別のリソースへ向けており、`Location` の URI が元のリクエストに対する間接的な応答を提供する」ものと定義し、後続のリクエストは GET を使うと規定しています。post-redirect-get に 302 を吐くフレームワークがほとんどですが、その意図をそのまま表しているコードは 303 のほうです。

302 と間違えられやすいコード

紛らわしいコード見分け方
307307 (Temporary Redirect) は、メソッドを保証した 302 です。RFC 9110 §15.4.3 は歴史的な理由から、302 に対してユーザーエージェントが POST を GET に変えることを許しています。§15.4.8 はまさにそれを起こさないために 307 を定義し、メソッドとボディをそのまま再送すると規定しました。一時性は同じ、契約が違う——そしてフォーム送信の途中にあるリダイレクトなら、その契約の中身はあなたのリクエストボディです。
303303 (See Other, §15.4.4) は「あなたのリクエストへの答えは別の URI にあり、後続は GET だ」と言います。post-redirect-get が本来意味しているのはこれで、302 が同じ挙動を「許している」だけなのに対し、303 はそれを明言します。POST が成功したあとにリダイレクトするなら、歴史的な許容に寄りかからず 303 で意図を書いてください。
301利用者に見える違いは恒久かどうかですが、運用に効くのはキャッシュです。302 は RFC 9110 §15.1 のヒューリスティックにキャッシュ可能なステータスコードの一覧に入っていないので、`Cache-Control` を明示しない限り保存されず、次のデプロイで宛先を変えられます。301 は保存され、訪問者のブラウザから消す手段はありません。302 で回して、確信が持てたら格上げしてください。
401期限切れセッションに、ログインページへの 302 で答える API は、あらゆる認証失敗を「HTML が詰まった 200」に変えてしまいます。コードが何かを見る前に、ブラウザがリダイレクトを辿り終えているからです。JSON ボディを添えた 401——そして RFC 9110 §15.5.2 に従うなら `WWW-Authenticate` ヘッダーも——なら、その失敗を処理しなければならないクライアントにとって読める形で残ります。
理由句
Found
クラス
3xx リダイレクト
定義
RFC 9110
標準
IETF 標準

このサイトのツール

どれもブラウザ内で完結します。アップロードは発生しません。

関連するステータスコード

302 を調べているとき、ついでに読むことになりがちなコードです。

よくある質問

このステータスコードを出したのがどのサーバーかを知るには?

ページではなく応答ヘッダーを読んでください。curl -sS -o /dev/null -D - https://example.com/path が本文なしでヘッダーだけを出し、serverviacf-ray の 3 つで、答えたのがどの層かが分かります。cf-ray の値があれば応答を扱ったのは Cloudflare で、その値はサポートが聞いてくる ID です。エッジを完全に外して確かめたいなら、--resolve example.com:443:203.0.113.10 を付けて同じリクエストを投げ直します。元のホスト名と SNI を送りつつ、指定したオリジンのアドレスへ接続するので、答えが変われば、エッジとオリジンの言い分が食い違っているということです。

数字のあとの理由句(Not Found など)に意味はありますか?

ありません。分岐条件にしてはいけません。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 を返せば、当番の人をスタックの間違った半分へ送り込むことになります。

302 でまだ詰まっているなら、ステータスコード一覧をすべて見る。あるいは上に戻って、自分の立ち位置に向けて書かれたブロックを読んでください。