リダイレクトの仕組みは単純に見えます。サーバーがステータスコードと Location ヘッダーを返し、クライアントがその URL に移動するだけです。しかし、この 3 桁の数字によって、ブラウザーがリダイレクトをキャッシュしてよいか、POST が POST のまま送られるか、検索エンジンがどの URL をインデックスに登録しやすいかが決まります。

このガイドでは、RFC 9110 で定義されている 3xx クラスのステータスコード、コード同士の役割が重なっている歴史的な経緯、ステータスコードを使わないリダイレクト、そして主要なサーバーで適切なコードを設定する方法を解説します。

3xx クラスの意味

HTTP のセマンティクスを定める現行の仕様である RFC 9110 によると、3xx のステータスコードは「リクエストを完了するために、ユーザーエージェントがさらなるアクションを取る必要がある」ことを示します。15.4 節では、これらのコードを 4 種類のリダイレクトに分類しています。

  • Location で示される別の URI でリソースを利用できる:301、302、307、308
  • クライアントが、リソースの複数の表現から選択できる:300
  • 別のリソースが、リクエストに対する間接的なレスポンスを提供する:303
  • クライアントが、キャッシュに保存済みのコピーを再利用できる:304

実際に自動リダイレクトとして機能するのは、301、302、303、307、308 だけです。ブラウザーが実装している Fetch Standard でも、まさにこの 5 つのコードを「redirect status」(リダイレクトステータス)と呼んでいます。300 は通常、普通のページと同じように表示され、304 はキャッシュ内のコピーを使うようクライアントに伝えます。

Location の値には、/new-page や ../shop/ のような相対参照も使えます。クライアントは、直前にリクエストした URL を基準にこれを解決します(RFC 9110 の 10.2.2 節)。Location にフラグメントがない場合は、元のフラグメントが引き継がれます。たとえば、/docs/guide にリダイレクトされる /guide#install へのリンクは、最終的に /docs/guide#install に到達します。

本来のリダイレクトである 5 つのコードのうち、どれが適しているかは次の 2 つの問いで決まります。

  1. 移転は恒久的か、一時的か:恒久的なリダイレクトはキャッシュされることがあり、元の URL の代わりに新しい URL をインデックスに登録するよう、検索エンジンに伝えるシグナルになります。
  2. リクエストメソッドを維持する必要があるか:元のリクエストが単純な GET ではなく、POST、PUT、DELETE の場合に重要になります。

リダイレクトのステータスコード一覧

コード 名前 恒久的 メソッドの維持 デフォルトでキャッシュ可能 主な用途 Google 検索
300 Multiple Choices – – はい 代替となる表現の提示(まれ) –
301 Moved Permanently はい いいえ(POST が GET に変わる場合あり) はい ページやサイトの移転、HTTPS 化、ホストの正規化 恒久的(強いシグナル)
302 Found いいえ いいえ(POST が GET に変わる場合あり) いいえ 短期間のリダイレクト、ログインウォール、A/B テスト 一時的(弱いシグナル)
303 See Other いいえ いいえ(GET に変わる) いいえ フォーム送信後の結果ページ 一時的
304 Not Modified リダイレクトではない – – 条件付きリクエストへの応答 コンテンツに変更なし
305 Use Proxy 非推奨 – – 使用しない –
306 (Unused) 予約済み – – 使用しない –
307 Temporary Redirect いいえ はい いいえ あらゆるメソッドの一時的なリダイレクト 一時的
308 Permanent Redirect はい はい はい API、フォーム、あらゆる URL の恒久的な移転 恒久的(強いシグナル)

「デフォルトでキャッシュ可能」とは、RFC 9110 の 15.1 節でいう heuristically cacheable(ヒューリスティックにキャッシュ可能)なコードであることを意味します。つまり、明示的な鮮度情報がなくても、キャッシュはサーバーに確認せずにレスポンスを再利用できます。なお、どのレスポンスでも、Cache-Control を使えばキャッシュ可能にも不可にもできます。「Google 検索」の列は、Google のリダイレクトと HTTP ステータスコードに関するドキュメントの内容をまとめたものです。

各ステータスコードの詳細

300 Multiple Choices

リソースに、異なる言語やファイル形式など複数の表現があり、クライアントがその中から選べることを示します。通常はボディに選択肢が列挙されますが、その形式は標準化されていません。推奨する選択肢を Location で指定することもできますが、ブラウザーがそれに従うとは限りません。実際には 300 はほとんど使われておらず、サーバーサイドのコンテンツネゴシエーションや、リンクを並べた普通のページのほうがうまく機能します。

301 Moved Permanently

対象のリソースに「新しい恒久的な URI が割り当てられており、今後このリソースを参照する際は、示された URI のいずれかを使うべき」であることを示します。ページの移転、ドメインの変更、HTTP から HTTPS への切り替え、正規ホストへのリダイレクトに使う標準的なコードです。

注意すべき点が 2 つあります。1 つ目に、301 はヒューリスティックにキャッシュ可能なので、ブラウザーがこれを記憶し、サーバーに問い合わせなくなることがあります(後述のキャッシュに関するセクションを参照)。2 つ目に、歴史的な理由から、クライアントは 301 をたどる際に POST を GET に変更してよいことになっており、ブラウザーは実際にそうします。GET で読み込むページには関係ありませんが、フォームの送信先や API には 308 を使ってください。

302 Found

対象のリソースが「一時的に別の URI に存在する」ことを示すため、クライアントは今後のリクエストでも元の URL を使い続けるべきです。302 はデフォルトではキャッシュされません。1 週間だけカテゴリページの代わりにセールページを表示する場合や、ログインウォール、地域によるルーティング、A/B テストなど、短期間の用途に使います。

301 と同様に、302 でも POST から GET への変更が認められています。多くのフレームワークはコードを指定しないと 302 を送信するため、恒久的な移転なのにうっかり 302 になってしまうケースが後を絶ちません。

303 See Other

303 は、元のリクエストに対する間接的なレスポンスとなる別のリソースにクライアントを誘導し、クライアントはそれを GET(または HEAD)で取得します。Post/Redirect/Get パターンのために設計されたコードで、サーバーはフォームの送信を処理した後、確認ページや結果ページの URL を付けて 303 を返します。こうすると、そのページを再読み込みしてもフォームが再送信されることはなく、無害な GET が繰り返されるだけです。

303 はデフォルトではキャッシュされず、元の URL が移転したことを意味するものでもありません。Google は 303 を 302 と同じように扱います。

304 Not Modified(リダイレクトではない)

304 は 3xx の番号を持ちながら、クライアントをどこにも転送しません。条件付きの GET または HEAD リクエスト、つまり ETag の値を指定した If-None-Match や、日付を指定した If-Modified-Since を含むリクエストに対する応答です。リソースが変更されていなければ、サーバーはボディなしで 304 を返し、クライアントはキャッシュ内のコピーを使います。

HTTP
GET /styles.css HTTP/1.1
Host: example.com
If-None-Match: "5f1c-3a2b"

HTTP/1.1 304 Not Modified
ETag: "5f1c-3a2b"
Cache-Control: max-age=3600

リダイレクトチェッカーは条件なしのリクエストを送信するため、通常は 304 を目にすることはありません。TrueURL で 304 をテストするには、まず通常のチェックを実行し、レスポンスヘッダーから ETag の値をコピーして、カスタムリクエストヘッダーで If-None-Match ヘッダーとして送信します。Googlebot にとって 304 は、前回のクロール以降コンテンツが変更されていないことを意味します。

305 Use Proxy と 306(Unused)

305 は、Location で指定したプロキシを経由してリクエストをやり直すよう、クライアントに指示するコードでした。レスポンスによってクライアントのプロキシ設定を変更できるのはセキュリティ上のリスクとなるため、RFC 7231 でこのコードは非推奨となり、現在のブラウザーは 305 に従いません。

306 は以前のバージョンの仕様で定義されていたコードで(Switch Proxy と記載されることが多い)、現在は予約済みです。どちらのコードも現代のアプリケーションで使うべきものではなく、目にした場合は何らかの設定ミスがあります。

307 Temporary Redirect

307 の意味は 302 と同じですが、1 点だけ追加の条件があります。クライアントはリクエストメソッドを変更してはなりません。POST は同じボディとともに、POST のまま新しい URL に再送信されます。そのため、API やフォームの送信先の一時的なリダイレクトには 307 が適しています。

Chrome のデベロッパーツールでは、http:// から https:// への 307 Internal Redirect として 307 を見かけることもあります。これはサーバーが送ったものではありません。ブラウザーが自らリクエストをアップグレードしたもので、通常はそのサイトについて保存している HSTS(HTTP Strict Transport Security)ポリシーが理由です。

308 Permanent Redirect

308 は 301 に相当する、メソッドを維持するコードです。恒久的で、ヒューリスティックにキャッシュ可能であり、クライアントはメソッドを変更してはなりません。2014 年に実験的な RFC 7238 で初めて公開され、2015 年に RFC 7538 で標準となり、現在は RFC 9110 に含まれています。

POST、PUT、DELETE リクエストを受け付けるエンドポイントを恒久的に移転する場合は、308 を使います。通常のページでは 301 と 308 のどちらを使っても構わず、Google はどちらも恒久的なリダイレクトとして扱います。

303、307、308 が存在する理由

301 / 302 と 303 / 307 / 308 の役割が重なっているのは、歴史的な経緯によるものです。

HTTP/1.0(RFC 1945、1996 年)では、301 Moved Permanently と 302 Moved Temporarily が定義されました。この仕様では、ユーザーエージェントはユーザーの確認なしに POST を自動的にリダイレクトしてはならないとしたうえで、一部のユーザーエージェントが誤って POST を GET に変えてしまうことにもすでに触れていました。その後もブラウザーは、この挙動を続けました。

HTTP/1.1 では、解釈の余地のない 2 つのコードが追加されました。RFC 2068(1997 年)では、常に GET に切り替える 303 See Other が導入されました。RFC 2616(1999 年)では、常にメソッドを維持する 307 Temporary Redirect が追加されるとともに、302 の名前が Found に変更され、ほとんどのユーザーエージェントが 302 を 303 のように扱っていることが明記されました。

それでも、メソッドを維持する恒久的なリダイレクトはありませんでした。この穴を埋めたのが、15 年後に登場した 308 です。その一方で、仕様は現実を受け入れました。RFC 7231(2014 年)以降、ユーザーエージェントは 301 や 302 の後に POST を GET に変更してもよいことになっています。

現在のブラウザーは、Fetch Standard に定められた次のルールを適用しています。

ステータス 元のメソッド ブラウザーが新しい URL へのリクエストに使うメソッド
301、302 POST GET(ボディなし)
301、302 その他のメソッド 同じメソッド
303 GET と HEAD 以外のすべてのメソッド GET(ボディなし)
303 GET または HEAD 同じメソッド
307、308 すべてのメソッド 同じメソッドとボディ

301 や 302 の後に書き換えられるのは POST だけで、PUT や DELETE はメソッドが維持されます。ただし、HTTP ライブラリによって挙動が異なるため、これに頼るのは危険です。メソッドが重要な場合は、必ず 307 か 308 を使いましょう。

3xx ステータスコードを使わないリダイレクト

meta refresh

HTML
<meta http-equiv="refresh" content="0; url=https://example.com/new-page">

ページそのものは 200 OK で配信され、ブラウザーは読み込んだ後にこのタグに従います。数値は遅延時間(秒)です。Google は、即時の meta refresh を恒久的なリダイレクト、遅延のある meta refresh を一時的なリダイレクトと解釈します。

時間指定の refresh は、アクセシビリティも損ないます。WCAG では、即時の meta refresh はクライアントサイドリダイレクトとして認められていますが(達成方法 H76)、時間指定のものは失敗例(F40)とされています。ユーザーが制御できないタイマーで、ページが切り替わってしまうためです。

JavaScript リダイレクト

HTML
<script>
  window.location.replace('https://example.com/new-page');
</script>

location.replace() はリダイレクト元のページをセッション履歴に残さないため、「戻る」ボタンを押したユーザーが再びリダイレクトに引き戻されることはありません。location.href に代入した場合は、履歴に残ります。

Google が JavaScript を実行するのは、クロールの後にページをレンダリングするときだけで、レンダリングは失敗することもあります。Google のドキュメントでも、JavaScript リダイレクトは、サーバーサイドリダイレクトや meta refresh によるリダイレクトが使えない場合にのみ推奨されています。ほかの多くのボットやツールは、JavaScript をまったく実行しません。

Refresh ヘッダー

HTTP
HTTP/1.1 200 OK
Content-Type: text/html
Refresh: 0; url=https://example.com/new-page

Refresh ヘッダーは、どの HTTP の RFC でも定義されていません。ブラウザーは数十年前からサポートしており、HTML Standard では meta refresh タグと同等のものとして扱われています。しかし、curl をはじめ多くの HTTP クライアントやツールはこれを無視するため、本来の 3xx リダイレクトを使うことをおすすめします。

リダイレクトを正しく実装する方法

nginx

nginx
# どちらのホストでも、http:// のリクエストはすべて 1 ホップで正規の HTTPS URL へ
server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://example.com$request_uri;
}

# https://www.example.com -> https://example.com
server {
    listen 443 ssl;
    server_name www.example.com;
    ssl_certificate     /etc/ssl/example.com.crt;
    ssl_certificate_key /etc/ssl/example.com.key;
    return 301 https://example.com$request_uri;
}

server {
    listen 443 ssl;
    server_name example.com;
    # ssl_certificate と ssl_certificate_key は上と同じ

    # 移動した 1 ページ
    location = /old-page {
        return 301 /new-page;
    }

    # セクション全体(残りのパスは維持)
    location /blog/ {
        rewrite ^/blog/(.*)$ /articles/$1 permanent;
    }
}

$request_uri には元のパスとクエリ文字列が含まれるため、情報が失われることはありません。rewrite ... permanent は 301 を、rewrite ... redirect は 302 を送信します。307 や 308 を使う場合は、そのコードを指定して return を使います。

Apache (.htaccess)

Apache
# mod_alias: 1 ページと、フォルダー全体(残りのパスは末尾に付加)
Redirect 301 /old-page https://example.com/new-page
Redirect 301 /blog/ https://example.com/articles/

# mod_alias で正規表現を使う場合
RedirectMatch 301 ^/products/([0-9]+)\.html$ https://example.com/p/$1

# mod_rewrite: http:// と www のリクエストをすべて 1 ホップで https://example.com へ
RewriteEngine On
RewriteCond %{HTTPS} off [OR]
RewriteCond %{HTTP_HOST} ^www\. [NC]
RewriteRule ^ https://example.com%{REQUEST_URI} [R=301,L]

Redirect はステータスコードを省略すると 302 を送信するため、必ずコードを明記してください。暗号化されていない HTTP で Apache に接続するロードバランサーや CDN の背後では、%{HTTPS} が常に off になり、最後のルールがループします。代わりに %{HTTP:X-Forwarded-Proto} を判定しましょう。mod_alias と mod_rewrite はそれぞれ独立して動作し、ファイル内の行の順序によって、どちらのモジュールが先に実行されるかが決まるわけではありません。この例の http://www.example.com/old-page のように両方のルールに一致する URL は、2 ホップ(2 回のリダイレクト)になることがあります。1 ホップでリダイレクトしなければならない URL については、そのルールをすべて 1 つのモジュールにまとめてください。

Cloudflare

Cloudflare では、リクエストがサーバーに届く前に、エッジでリダイレクトできます。

  • Single Redirects(ダッシュボードの Redirect Rules セクション)は、フィルター式でリクエストを照合し、301、302、303、307、308 のいずれかで、静的または動的なリダイレクト先に転送します。www からルートドメイン(apex)へリダイレクトする典型的なルールの設定は次のとおりです。

    When incoming requests match:  (http.host eq "www.example.com")
    URL redirect type:             Dynamic
    Expression:                    concat("https://example.com", http.request.uri.path)
    Status code:                   301
    Preserve query string:         On
  • Bulk Redirects は、CSV でアップロードした移行マップのような、転送元と転送先の URL の長いリストを処理します。エントリごとにステータスコードや、クエリ文字列を維持するかどうかといったオプションを設定でき、Bulk Redirect Rule によってリストが有効になります。

  • Workers では、Response.redirect() を使ってコードでリダイレクトを作成できます。

JavaScript
export default {
  async fetch(request) {
    const url = new URL(request.url);
    if (url.pathname === '/old-page') {
      url.pathname = '/new-page'; // クエリ文字列は維持される
      return Response.redirect(url.toString(), 301);
    }
    return fetch(request);
  },
};

Response.redirect() のデフォルトは 302 で、指定できるのは 301、302、303、307、308 だけです。それ以外のコードを指定すると、RangeError がスローされます。

Node.js と Express

JavaScript
import express from 'express';

const app = express();
app.set('trust proxy', 1); // ロードバランサーや CDN の背後で動かす場合

// HTTPS 化と正規ホストへの統一を 1 ホップで
app.use((req, res, next) => {
  if (req.secure && req.hostname === 'example.com') return next();
  res.redirect(301, `https://example.com${req.originalUrl}`);
});

// 移動した 1 ページ
app.get('/old-page', (req, res) => {
  res.redirect(301, '/new-page');
});

res.redirect(url) はステータスを省略すると 302 を送信します。Express 5 でサポートされている引数の順序は、res.redirect(status, url) だけです。素の Node.js では、ステータスとヘッダーを自分で設定します。

JavaScript
res.writeHead(301, { Location: 'https://example.com/new-page' });
res.end();

PHP

PHP
<?php
// 出力を送信する前に実行する必要があります。
header('Location: https://example.com/new-page', true, 301);
exit;

PHP では、Location ヘッダーだけを設定すると(201 や 3xx のステータスがすでに設定されている場合を除き)302 が送信されます。そのため、3 番目の引数でコードを指定し、それ以降の処理が実行されないよう exit してください。WordPress の wp_redirect() と wp_safe_redirect() もデフォルトは 302 なので、2 番目の引数に 301 を渡します。

適切なリダイレクトコードの選び方

  • ページが恒久的に移転した場合で、GET で読み込む通常のページなら、301 を使います。308 でも同じように機能します。
  • 移転が恒久的で、API、フォームの送信先、Webhook のように、クライアントが POST、PUT、DELETE を送信する可能性がある場合は、308 を使います。
  • リダイレクトが一時的で、GET だけを考えればよい場合は、302 を使います。
  • リダイレクトが一時的で、メソッドを維持する必要がある場合は、307 を使います。
  • フォームなどの POST を処理した後で、ユーザーに結果ページを表示したい場合は、303 を使います。
  • HTTP から HTTPS へのリダイレクトには、301 または 308 を使います。さらに、HTTPS のサイトから Strict-Transport-Security ヘッダーを送信すれば、次回以降、ブラウザーは安全でないリクエストを省略します。
  • コンテンツが削除され、代わりになるページもない場合は、リダイレクトしないでください。404 または 410 を返します。トップページなど無関係なページへのリダイレクトは、Google にソフト 404 として扱われることがあります。
  • 300、305、306 は使わないでください。304 は条件付きリクエストに対してサーバーやキャッシュが生成するもので、リダイレクトとして選ぶコードではありません。

301 と 308 のキャッシュの落とし穴

301 と 308 はヒューリスティックにキャッシュ可能なため、ブラウザーがこれらを保存し、サーバーに問い合わせなくなることがあります。Chromium ベースのブラウザーは、キャッシュ関連のヘッダーがない 301 や 308 を、エントリがキャッシュから追い出されるか、ユーザーがキャッシュを消去するまで保持します。誤った恒久的なリダイレクトを設定すると、サーバー側を修正した後も長い間、以前の訪問者を誤った場所に送り続けることがあり、訪問者のブラウザーからそれを確実に取り消す方法はありません。

安全に運用するためのポイントは次のとおりです。

  • 新しいルールはまず 302 か 307 でテストし、リダイレクト先が確定してから 301 か 308 に切り替えます。
  • あるいは、導入期間中は、恒久的なリダイレクトをキャッシュできる期間を制限します。
nginx
location = /old-page {
    add_header Cache-Control "max-age=3600";
    return 301 https://example.com/new-page;
}
  • テストの際は、プライベートウィンドウや、Disable cache(キャッシュを無効化)をオンにしたデベロッパーツール、またはブラウザーのキャッシュを一切使わないサーバーサイドのチェッカーを使います。
  • CDN やプロキシもリダイレクトを保存することがあるため、リダイレクトを変更したら、それらのキャッシュも削除(パージ)します。
  • キャッシュされた A → B のリダイレクトを、B から A へのリダイレクトで打ち消そうとしないでください。最初のリダイレクトをブラウザーにキャッシュしている訪問者が、ループに陥るおそれがあります。

リダイレクトのテスト

どのプラットフォームを使っていても、結果は外部から確認しましょう。301 リダイレクトチェッカーは恒久的な移転のすべてのホップが 301 または 308 であることを確認し、リダイレクトチェーンチェッカーは各ホップを Location やヘッダーとともに表示します。HTTPS リダイレクトチェッカーでは、プロトコルとホストの 4 つのバリエーションすべてを一度にテストできます。Meta Refresh チェッカーはヘッダーしか見ないツールでは見落とされるクライアントサイドリダイレクトを検出し、リダイレクト速度チェッカーは各ホップにかかる時間を測定します。

ターミナルからは、curl -sIL https://example.com/old-page ですべてのホップのヘッダーを表示できます。コマンドライン、デベロッパーツール、よくあるリダイレクトの問題については、URL リダイレクトの確認方法に関するガイドで詳しく解説しています。