302
一時的な迂回です。リソース自体は要求した URL のままそこにあるけれど、今回だけは別の場所から返したい——という意味で、しかもブラウザはその迂回をあなたのコードに見せないまま辿ります。
同じ 3 桁でも、リクエストのどちら側にいるかで別々の問題になります。自分に当てはまるブロックを読んでください。
これが出ている理由
302 の厄介なところは、そもそも見えないことです。`fetch()` は既定でリダイレクトを辿りますし XHR には選択肢すらないので、HTML のログインページへ跳ばされた API 呼び出しは*成功した* 200 として解決し、本文は HTML。実際に観測できるエラーは `res.json()` が投げる `SyntaxError: Unexpected token '<'` で、指し示す先は切れたセッションではなくあなたのパーサーです。2 つ目の症状はボディの消失です。302 に対してブラウザは POST を GET として投げ直すことが許されているので、サーバーには payload のない GET が記録され、送ったはずのフィールドは全部消えます。3 つ目はオリジン跨ぎ。リダイレクト先は新しいリクエストで、CORS を単独で満たす必要があるため、`Access-Control-Allow-Origin` を返さない別ホストへ跳ばされると、呼んだ URL に対する CORS エラーとして報告され、原因のホップは痕跡ごと消えます。
やること
パースする前に `response.redirected` を確認し、`response.url` を要求した URL と比べてください。違っていたら両方ログに出します。`content-type` も見てください。JSON を頼んだのに HTML が返っているなら、それは名前を変えたログイン跳ね返しです。DevTools では「Preserve log」を有効にして、リダイレクトが引き起こしたナビゲーションで行が消えないようにします。サーバー側が自分の管轄なら、直すべきは上流です。API は期限切れセッションに対して、ログインページへの 302 ではなく 401 と JSON ボディで答えるべきです。クライアントが「サインインし直してください」と「サーバーが文書を送ってきた」を区別できるのは前者だけだからです。自分の管轄でないなら、`redirect: "manual"` が opaque-redirect 応答——status 0、type `"opaqueredirect"`、`Location` は読めない——を返してくれます。跳ばされたことを検知してサインインを促すには足りますが、行き先を知るには決して足りません。
これが出ている理由
302 が意図どおりでクライアントの着地点が違うか、そうでなければループです。着地点違いはプロキシの問題です。アプリケーションは自分が受け取ったつもりのリクエストから絶対 URL を組み立てるので、TLS を終端するプロキシの裏では `http://` を吐き、コンテナの中では内部ホスト名や外には存在しないポートを吐きます。nginx は同じ失敗に `absolute_redirect`・`server_name_in_redirect`・`port_in_redirect` と 3 つの名前を付けていますし、アプリケーションフレームワークのほうは `X-Forwarded-Proto` と `X-Forwarded-Host` が転送され、かつ信頼される設定になって初めて正しく組み立てられます。ループのほうはほぼ確実に Cookie です。`/login` へリダイレクトし、ログイン応答がセッション Cookie を発行し、戻りのリダイレクトがまた `/login` へ落ちる——Cookie が保存されなかったからです。平文 HTTP に載せた `Secure`、`Secure` なしの `SameSite=None`、一致しない `Domain`、あるいは Cookie を発行したホストとは別のホストへ跨ぐリダイレクト。原因はこのあたりです。
やること
ソースに書いてある `Location` ではなく、実際に線を流れた `Location` を読んでください。この 2 つが食い違うのは、バグが本物のときだけです。次にプロキシの契約を両端から確認します。プロキシは `X-Forwarded-Proto` を送っているか、フレームワークはそれを信頼する設定になっているか。ループについては、ログイン応答の `Set-Cookie` を見たうえで、*次の*リクエストがそもそも `Cookie` ヘッダーを持っていたかどうかを見ます。持っていなければ属性がバグであって、リダイレクトのロジックをいくらいじっても直りません。最後に、フレームワークの既定を受け入れるのではなくコードを選び取ってください。素の一時的な迂回なら 302、POST のあとで後続を GET と規定したいなら 303、メソッドとボディをそのまま再送させたいなら 307 です。
これが出ている理由
サイトが意図的にあなたを別の場所へ送っていて、それは一時的なもののはずです。サインインページ、国や言語ごとの版、メンテナンスのお知らせ。自分で打っていないアドレスに変わるのはリダイレクトが働いている証拠で、乗っ取られたわけではありません。失敗の形は跳ね返りです。サインインしたのにまたサインインページに戻る、それが延々と続く、あるいはブラウザが「リダイレクトが多すぎます」で止まる。これはサイト側のセッション処理の問題で、犯人はたいてい、ブラウザが保持できていない Cookie です。
やること
ログインページに戻され続けるなら、そのサイトの Cookie を許可してください。このループはほぼ確実に、サインイン用の Cookie がブロックされたか消されたかを意味しています。Cookie を自動で消す拡張機能が入っていないかを確認し、そのサイトの Cookie を許可した状態のプライベートウィンドウで試してみてください。そこで通るなら、差は拡張機能かサイト個別の Cookie 設定です。望んでいない国や言語の版へ飛ばされる場合は、アドレスを書き換えるのではなく、着地したページにある言語切り替えのリンクを使ってください。アドレスを直しても、次のリクエストで同じリダイレクトがまた発火します。
失敗した URL に対してそのまま実行してください。エラーページを表示せずにステータスだけを出すので、ブラウザが描画したものではなくサーバーが言ったことが見えます。
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` を付けて実行し直してサーバーのログを見比べれば、フィールドを落としたのがメソッドの書き換えかどうかを証明できます。
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 のほうです。
| 紛らわしいコード | 見分け方 |
|---|---|
307 | 307 (Temporary Redirect) は、メソッドを保証した 302 です。RFC 9110 §15.4.3 は歴史的な理由から、302 に対してユーザーエージェントが POST を GET に変えることを許しています。§15.4.8 はまさにそれを起こさないために 307 を定義し、メソッドとボディをそのまま再送すると規定しました。一時性は同じ、契約が違う——そしてフォーム送信の途中にあるリダイレクトなら、その契約の中身はあなたのリクエストボディです。 |
303 | 303 (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` ヘッダーも——なら、その失敗を処理しなければならないクライアントにとって読める形で残ります。 |
どれもブラウザ内で完結します。アップロードは発生しません。
302 を調べているとき、ついでに読むことになりがちなコードです。
ページではなく応答ヘッダーを読んでください。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 を返せば、当番の人をスタックの間違った半分へ送り込むことになります。
302 でまだ詰まっているなら、ステータスコード一覧をすべて見る。あるいは上に戻って、自分の立ち位置に向けて書かれたブロックを読んでください。