ページを開こうとしたら、「このページは動作していません」「example.com でリダイレクトが繰り返し行われました」という画面が出て、先に進めない。Chrome では、エラーコードとして ERR_TOO_MANY_REDIRECTS が表示されます。「307 エラーが出た」という相談の多くは、実際にはこの状態を指しています。

画面に出ているのはエラーですが、その元になっているのは転送が終わらずに続いていることです。一度の 307 や 301 は正常な案内でも、行き先が元の場所へ戻る循環ができてしまうと、ブラウザーは途中で通信を打ち切ります。原因は、転送を返している番号ではなく、複数の設定の組み合わせにあります。

症状

転送が循環して、ブラウザーが途中で通信を打ち切ります。

切り分け

curl で転送の連鎖を一覧にすると、循環の位置が分かります。

主な原因

http と https の食い違い、www の有無、CMS の URL 設定、プロキシです。

仕様上の位置づけ

RFC 9110 は、クライアントが循環を検出して介入すべきとしています。

原因が分かりにくいと、時間を失う

リダイレクトループの厄介な点は、原因が一つの設定に収まらないことです。

転送の設定は、さまざまな場所に分散しています。Web サーバーの設定、アプリケーション(CMS やフレームワーク)の設定、CDN やロードバランサー、ブラウザーに残った HSTS の記憶。どれか一つだけを見ても、設定は正しく見えることがあります。ところが、組み合わせると「A が B へ、B が A へ」という循環ができてしまいます。

さらに、自分の環境では再現して、他の環境では再現しないことがあります。ブラウザーに保存されたキャッシュや Cookie、HSTS の状態が絡むためです。「直ったと思ったのに、まだ表示されない」という場面が頻発します。

また、運営者にとっては、検索エンジンのクローラーにも影響が出る点が見過ごせません。Google のクロールの文書によれば、Google のクローラーは既定で最大 10 回まで転送を追いかけます。それを超えると、転送の失敗として扱われます。ループが長く放置されると、ページが正しく処理されない状態が続きます。

こうした事情から、場当たり的に設定を変えるのではなく、手順を決めて切り分けることが、時間の節約につながります。

ブラウザーはなぜ打ち切るのか

そもそも、転送は何回まで追うのでしょうか。

WHATWG の Fetch Standard は、ブラウザーの通信の手順を定めています。リダイレクトを処理するたびに「転送回数」を数え、回数がすでに 20 に達していたら、ネットワークエラーとして終了します。この上限を超える連鎖は、事実上、ループとみなされます。

RFC 9110 も、クライアントは循環するリダイレクトを検出して介入すべき(SHOULD)としています。人間が目で確かめなくても、ソフトウェアが自動で止めるべきだ、という考え方です。

ほかのツールでは、上限が異なります。curl の場合、--max-redirs の既定値は 50 回です。同じ循環でも、ブラウザーは 20 回で、curl は 50 回で止まります。ツールごとに数が違うことは、切り分けのときに知っておくと便利です。

ループの代表的な原因

実際の現場で多い原因を挙げます。

原因 1: http と https が互いに転送している

もっとも多い原因です。サーバーは「http を https へ転送」し、アプリケーションは「https を http へ転送」している、という状態です。

リバースプロキシや CDN を使うと、さらに起きやすくなります。利用者はプロキシまで https で通信し、プロキシから Web サーバーへは http で通信する構成です。Web サーバー側には、すべてが http に見えるため、「http だから https へ転送しよう」と判断します。ところが、転送先の https もプロキシ経由で、再び Web サーバーには http として届くため、転送が止まりません。

clientserver

GET https://example.com/ (利用者 → プロキシ)
プロキシは、Web サーバーへ http で中継します
GET http://origin/ (プロキシ → Web サーバー)
301 / Location: https://example.com/
利用者に戻され、同じ流れが繰り返されます
GET https://example.com/

原因 2: www の有無が食い違っている

Web サーバーが「www なし」へ正規化し、アプリケーションが「www あり」へ正規化している場合です。お互いに、相手の URL を誤りとみなして転送し合います。

原因 3: アプリケーションの URL 設定とサーバー設定の不一致

CMS やフレームワークには、サイトの URL を登録する設定があります。ここで登録した URL(http か https か、www の有無、末尾のスラッシュ)と、サーバー側の転送設定が食い違うと、循環が起きます。サイトを https に移行した直後や、ドメインを変更した直後に頻発します。

原因 4: HSTS の記憶とサーバーの設定の衝突

サーバーが https から http へ転送する設定を持ったまま、ブラウザーが HSTS を記憶していると、ブラウザーは http を https に書き換え、サーバーは https を http に戻す、という往復になります。307 Internal Redirect と HSTSで説明したとおり、HSTS はブラウザー内で書き換えを行うため、サーバーのログには現れません。

未ログインの利用者をログインページへ転送し、ログイン後に元のページへ戻す仕組みで、Cookie が保存されないとループになります。Cookie がブラウザーの設定で拒否されている場合や、Cookie のドメインや Secure 属性が実際の URL と合っていない場合に起こります。ログイン状態を示す Cookie が届かないため、毎回「未ログイン」と判断されて、ログインページへ戻されるのです。

原因 6: 転送ルールそのものの書き間違い

正規表現が広すぎて、転送先の URL も転送の対象に含んでしまう場合があります。たとえば「/blog/ で始まる URL を /blog/ 配下へ転送する」ような規則は、転送先が再び同じ規則に当てはまるため、永久に続きます。

手順を決めて切り分ける

原因が多岐にわたっても、切り分けの手順は共通です。

手順 1: curl で転送の連鎖を見る

ブラウザーを使わずに、転送の連鎖を確認します。-I でヘッダーだけを取得し、-L で転送を追い、--max-redirs で回数を絞ると、循環を短く再現できます。

curl -sIL --max-redirs 8 https://example.com/ | grep -iE '^(HTTP|location)'

出力例のように、同じ URL が繰り返し並んでいたら、そこが循環の中心です。

HTTP/2 301
location: http://example.com/
HTTP/1.1 301
location: https://example.com/
HTTP/2 301
location: http://example.com/

この例では、https と http のあいだで転送が往復しています。どの URL がどの URL を指しているのかが、一目で分かります。

回数だけを確認したい場合は、次のコマンドが使えます。

curl -sL --max-redirs 20 -o /dev/null -w '%{num_redirects} %{url_effective}\n' https://example.com/

手順 2: 転送を返しているのが誰かを特定する

連鎖の各段階で、どこが転送を返したのかを調べます。同じ URL でも、Web サーバー、アプリケーション、CDN のどこが返したかで、直す場所が変わります。応答ヘッダーの Server や Via、X-Powered-By などが手がかりになります。

手順 3: ブラウザーの状態を切り分ける

curl では再現しないのに、ブラウザーでは起きる場合は、ブラウザー側の状態が原因です。シークレットウィンドウで開く、Cookie を削除する、HSTS の記憶を確認する、といった確認をします。Chromium 系のブラウザーでは、chrome://net-internals/#hsts でドメインの HSTS の状態を調べられます。

手順 4: 開発者ツールで、ログを残して見る

開発者ツールのネットワーク画面で「ログを保持」を有効にすると、転送ごとにページ遷移でログが消えず、連鎖の全体が確認できます。各行の Location ヘッダーを追うと、循環の経路が分かります。

原因別の直し方

http と https の食い違い(プロキシがある場合)

プロキシが付けてくれる X-Forwarded-Proto ヘッダーを使い、利用者が実際に使った通信方式で判断します。Web サーバーが見ている通信方式ではなく、元の通信方式を基準にします。nginx の例を示します。

server {
    listen 80;
    server_name example.com;

    # プロキシ経由で、利用者が http でアクセスしてきたときだけ https へ転送する
    if ($http_x_forwarded_proto = "http") {
        return 308 https://$host$request_uri;
    }
    # ... 以降は通常のサイト設定
}

アプリケーション側にも、プロキシの背後にいることを伝える設定(信頼するプロキシの指定)があります。フレームワークごとの設定名は異なるため、お使いのものの説明を確認してください。

www の有無の食い違い

正規化は、一か所だけで行います。Web サーバーで行うなら、アプリケーション側の正規化は無効にします。逆も同様です。どちらで行うかを決めたら、サイトの URL 設定も合わせます。

転送ルールの書き間違い

規則が転送先に当てはまらないかを確認します。条件を足して、すでに目的のパスにいる場合は転送しないようにします。

Cookie のドメイン、パス、Secure 属性が、実際の URL と合っているかを確認します。https でしか送られない設定(Secure)の Cookie を、http のページで期待していないかも見直します。

直したあとの確認

修正したら、次の 3 点を確認します。

  1. curl -sIL で、転送が 1 回、あるいは 0 回で最終ページに到達すること。
  2. 想定した最終 URL に着いていること(url_effective で確認)。
  3. シークレットウィンドウで、ブラウザーからも開けること。

恒久的な転送(301 や 308)を誤って公開していた場合は、ブラウザーが転送を保存していることがあり、修正しても古い転送が残ることがあります。その場合は、ブラウザーのキャッシュを消して確認してください。

再発を防ぐ設計の考え方

一度直しても、設定変更のたびに再発しては意味がありません。ループを起こしにくくする考え方を、4 つ挙げます。

  • 正規の URL を一つ決める。 「https で、www なし、末尾のスラッシュなし」のように、サイトの正規の形を文書で決めておきます。迷ったとき、設定の食い違いを判断する基準になります。
  • 転送は一か所で行う。 Web サーバー、アプリケーション、CDN のうち、どこで転送するかを決め、他では行いません。複数の場所に同じ役割の設定があると、食い違いの温床になります。
  • 旧 URL から最終 URL まで、転送は一度だけにする。 「http の www なし → https の www なし → https の www あり」のように連なる転送は、一度で最終地点に着くように設計し直します。表示も速くなり、循環の危険も減ります。
  • 変更のたびに curl で確認する。 サイトの URL、証明書、CDN、プロキシの設定を変えたら、curl -sIL で転送の連鎖を必ず確認します。確認のコマンドを手順書に載せておくと、担当者が替わっても同じ確認ができます。確認の結果は、変更の記録とあわせて残しておくと、後から不具合が出たときに、どの変更が原因かをたどりやすくなります。

とくに、サイトの https 移行やドメインの変更といった大きな作業のあとは、ループが生まれやすい時期です。公開直後には、必ず複数の入り口(http と https、www の有無、末尾のスラッシュ)から順にアクセスして、すべてが一度の転送で正規の URL に着くことを確かめておくと安心です。スマートフォンなど、普段と異なる環境からも一度開いてみると、Cookie や記憶の違いによる見落としを減らせます。

循環は「設定の組み合わせ」を見直す合図

要点を整理します。

  • ループの画面は、転送が終わらないために出るエラーで、307 や 301 の番号そのものが悪いわけではありません。
  • ブラウザーは転送回数が 20 に達すると打ち切ります。curl の既定は 50、Google のクローラーは既定で 10 回までです。
  • 原因は、http と https、www の有無、アプリケーションの URL 設定、プロキシ、HSTS、Cookie などの組み合わせにあります。
  • 切り分けは、curl で連鎖を見る → 返している主体を特定する → ブラウザーの状態を除く、の順に進めます。
  • 正規化は、一か所に集約します。

転送の番号ごとの意味をあらためて確認したい場合は、301・302・303・307・308 の違いが参考になります。