301
古い URL は退役し、`Location` に入っている URL がその座を引き継ぎます。しかも 301 は、何も指示しなくてもキャッシュが保存してよい数少ない応答の一つなので、間違って出した 301 はいちばん取り消しにくいリダイレクトになります。
同じ 3 桁でも、リクエストのどちら側にいるかで別々の問題になります。自分に当てはまるブロックを読んでください。
これが出ている理由
301 そのものを目にすることはまずありません。`fetch()` は既定で `redirect: "follow"` ですし、XHR には選択の余地すらないので、コードが受け取るのは*別の* URL からの応答で、`response.redirected === true`、`response.url` は自分が指定した URL ではなくなっています。その裏で 2 つのことが起きます。まず、POST が GET として投げ直されているかもしれません——301 に対して RFC 9110 はまさにそれを許しています——ので、サーバーのログにはボディのない GET が残り、ハンドラが呼ばれていないのではなく壊れているように見えます。もう一つはオリジン跨ぎです。リダイレクト先は新しいリクエストなので CORS を単独で満たす必要があり、`Access-Control-Allow-Origin` を返さないホストへ飛ばされると、ブラウザは元の URL に対する CORS エラーとして報告し、ホップの存在自体を隠してしまいます。おまけにブラウザはリダイレクトをキャッシュするので、一度まずいほうを読み込んでしまえば、サーバーが直ったあともあなたの端末だけが同じ回り道を続けます。
やること
ペイロードを信用する前に `response.redirected` と `response.url` を読み、要求した URL と食い違ったらログに出してください。その 1 行で、謎の挙動が「消しに行けるリダイレクト」に変わります。DevTools では「Disable cache」を入れて「Preserve log」を有効にします。ナビゲーション単位のリダイレクトは、その直後のナビゲーションでパネルから消えてしまうからです。そのリダイレクトがまだサーバー側に存在するのかを確かめるには、同じ URL を curl に通します。curl はキャッシュを持たないので、ブラウザだけがリダイレクトして curl がしないなら、そのリダイレクトはサーバーではなくあなたの端末に保存されています。API 呼び出しの中でリダイレクトに頼るのはやめて、クライアントは正規の URL に向けてください。往復 1 回は回り道ではなくリクエストそのものに使うべきです。ブラウザで `redirect: "manual"` をデバッグに使うのは筋が悪いです。返ってくるのは status 0 の opaque-redirect 応答で `Location` は読めず、「リダイレクトされた」ことだけは分かっても、どこへ行ったのかは決して分かりません。
これが出ている理由
出してしまって後悔している 301 か、リダイレクト自体はあるが行き先が違うかのどちらかです。行き先違いはほぼプロキシです。アプリケーションは「自分が受け取ったリクエストはこうだったはず」という認識から絶対 URL を組み立てるので、TLS を終端するプロキシの裏では、スキームは `http`、ホストは内部名だと思い込みます。nginx はこの同じ種類のバグに `absolute_redirect`・`server_name_in_redirect`・`port_in_redirect` と 3 つの名前を用意していて、末尾に `:8080` が付いてくるリダイレクトは最後のものです。ループのほうには、他を調べる前に確認する価値のある有名な原因があります。Cloudflare は、Flexible 暗号化モードと、HTTP を HTTPS へリダイレクトするオリジンの組み合わせが無限リダイレクトループになると明記しています。オリジンには常に平文の HTTP しか見えないので、すでに HTTPS で来ているリクエストにも「HTTPS へ行け」と言い続けるからです。
やること
行き先をまだ決めきれていないうちは 302 でリダイレクトして、宛先が確定してから 301 に格上げしてください。302 は既定ではキャッシュされず、301 はされるからです。301 を出すときは `Cache-Control: max-age=` を明示して添え、影響範囲にヒューリスティックではない期限を与えます。プロキシの裏では `X-Forwarded-Proto` と `X-Forwarded-Host` を転送したうえで信頼する設定にし、フレームワークが自分をどのスキームで応答していると思っているかを確認します。あるいは相対 `Location`(`absolute_redirect off;`)にして、絶対 URL の話そのものを消してしまう手もあります。すでに出回った 301 を取り消したければ逆向きのリダイレクトを出すことになりますが、それで何が買えるのかは正直に見積もってください。効くのは保存済みのコピーが期限切れになったクライアントと、そもそも保存しなかったクライアントだけで、他人のブラウザに居座っているリダイレクトをパージする手段はありません。
これが出ている理由
サイトがそのページを移動させ、ブラウザは新しいアドレスへ行くよう指示されています。自分で打っていないアドレスがアドレスバーに出るのはそのためで、これは正常な動作です。おかしいのは 2 つの形です。明らかに見当違いの場所へ飛ぶ場合と、2 つのルールが互いを指していてブラウザが「リダイレクトが多すぎます」で諦める場合。どちらもサイト側の設定の問題で、あなたの端末のせいではありません。そして、サイトが直ったあとも間違ったほうが残ることがあるのは、恒久的なリダイレクトはブラウザが覚えて確認なしに繰り返してよいものだからです。
やること
まず同じアドレスをプライベートウィンドウ(シークレットウィンドウ)で開いてください。そのウィンドウは覚えたリダイレクトを持たない状態で始まるので、そこで開けるなら古いルールはあなたの端末側にあり、ブラウザの設定でそのサイトのキャッシュされたファイルを消せば片付きます。Chrome なら「プライバシーとセキュリティ」の中にあり、保存されたリダイレクトを消すのは「キャッシュされた画像とファイル」の削除です。プライベートウィンドウでもループするなら、手元でできることはありません。ループはサイト側にあります。運営に伝えるなら、最初に開いたアドレスと、最終的に行き着くアドレスの 2 つがいちばん役に立ちます。
失敗した URL に対してそのまま実行してください。エラーページを表示せずにステータスだけを出すので、ブラウザが描画したものではなくサーバーが言ったことが見えます。
curl -sS -D - -o /dev/null -L -w '\n%{num_redirects} hops, final %{http_code} at %{url_effective}\n' http://example.com/old-page`-D -` は最後のホップだけでなく全ホップの応答ヘッダーを標準出力に落とし、`-o /dev/null` は本文を全部捨て、`-L` は連鎖を辿り、`-w` はホップ数・最終ステータス・それを返した URL を出します。上から順に読んでください。各ステータス行は、そのホップが 301 だったか 302 だったかを教えます。これはキャッシュが保存してよいかどうかの分かれ目です。各 `location:` は宛先の形を教えます。HTTPS へ戻すサイトなのに絶対 `http://` になっていればループですし、内部ホスト名や紛れ込んだ `:8080` は、プロキシがアプリケーションにリクエストの内容を誤って伝えている印です。そして 301 の応答自体に `cache-control` か `expires` があるかを見てください。どちらも無ければ、RFC 9110 §15.1 のヒューリスティックにキャッシュ可能なままで、訪問者が抱え込むのはそのバージョンのリダイレクトです。決定打になる手が 2 つあります。curl はキャッシュを持たないので、ブラウザはリダイレクトするのにこのコマンドがしないなら、そのリダイレクトはあなたの端末にしか存在しません。もう 1 つ、curl は 301・302・303 を辿るとき POST を GET に落とすと自ら明記しているので、同じコマンドを `--post301` 付きで実行してサーバーのログを見比べれば、リクエストボディを食べているのがこのリダイレクトかどうかを証明できます。
RFC 9110 §15.4.2 は 301 を、対象リソースに新しい恒久的な URI が割り当てられた状態と定義し、サーバーは推奨する URI を `Location` フィールドに入れて返すこと、そしてユーザーエージェントが手元に持っている古い URI への参照を書き換えてよいことまで明記しています。この定義のうち、実際に人がつまずくのは 3 点です。1 つ目、「恒久的」は設定ファイルに書いたメモではなく、キャッシュに対する約束です。RFC 9110 §15.1 は 301 をヒューリスティックにキャッシュ可能なステータスコードの一つに挙げているので、`Cache-Control` を一切付けていない 301 でも、CDN にもプロキシにもブラウザ自身にも保存され、二度とあなたに確認せず再利用されえます。2 つ目、メソッドは保証されません。§15.4.2 は歴史的な理由からユーザーエージェントが後続のリクエストで POST を GET に変えてもよいと書いていて、その挙動が困る場合に使うコードとして 308 (RFC 7538) を名指ししています。3 つ目、`Location` は URI-reference であり、RFC 9110 §10.2.2 は相対参照を許して対象 URI を基準に解決させます。つまり設定上は正しく見えるリダイレクトでも、それを組み立てた何かがリクエストについて誤った情報を渡されていれば、スキームもホストもポートも違う場所へ着地しえます。そして仕様のどこにも、すでにどこかへ保存された 301 を撤回する手段はありません。今日その 301 を出すかどうかは、結局この一点で決めるべきです。
| 紛らわしいコード | 見分け方 |
|---|---|
308 | 308 (RFC 7538) は、メソッドとボディをそのまま再送することを保証する恒久リダイレクトです。RFC 9110 §15.4.2 が 301 について「POST を GET に変えてよい」と認めて手放した保証が、まさにこれです。既定でキャッシュ可能なのは両方なので、308 も同じくらい後を引きます。選ぶ基準は寿命ではなくメソッドのほうです。 |
| 302 | 目に見える違いは恒久かどうかですが、金額に効くのはキャッシュ可能性です。301 は RFC 9110 §15.1 の「ヒューリスティックにキャッシュ可能なステータスコード」の一覧に入っていて、302 は入っていません。つまり `Cache-Control` を明示しない 302 は保存されず、次のリクエストで宛先を自由に変えられます。確信が持てるまで 302 で回し、決まってから格上げする——根拠はこれだけです。 |
HSTS | `http://` から `https://` への切り替えが、いつもサーバーのリダイレクトとは限りません。一度そのホストが `Strict-Transport-Security` を送っていれば、ブラウザはリクエストが端末を出る前に URL を書き換えます。Chrome のネットワークパネルでは `307 Internal Redirect` と `Non-Authoritative-Reason: HSTS` として見えます。サーバー側に対応する設定は存在しないので、原因のリダイレクトを設定ファイルから探しても永遠に見つかりません。 |
Cloudflare | リダイレクトは、オリジンに問い合わせる前にエッジで発行されることがあります。Cloudflare の Always Use HTTPS、Bulk Redirects、Redirect Rules はどれもそうです。だからサーバーの設定のどこにも書いていない 301 が実在します。対になる罠は逆向きで、Cloudflare 自身が、Flexible 暗号化と HTTP から HTTPS へリダイレクトするオリジンの組み合わせは無限ループになると書いています。オリジンには、自分が要求している HTTPS が一度も見えないからです。 |
どれもブラウザ内で完結します。アップロードは発生しません。
301 を調べているとき、ついでに読むことになりがちなコードです。
ページではなく応答ヘッダーを読んでください。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 を返せば、当番の人をスタックの間違った半分へ送り込むことになります。
301 でまだ詰まっているなら、ステータスコード一覧をすべて見る。あるいは上に戻って、自分の立ち位置に向けて書かれたブロックを読んでください。