HTTPのリダイレクト

2021/03/01更新

目次

概要

HTTPリダイレクトに関して、忘れがちな情報をここにまとめておく。

HTTPリダイレクト

HTTPリダイレクトの一覧

HTTPリダイレクトには以下のような種類がある。

HTTPステータスコード

恒久的/一時的

メソッドの変更

経緯

301 Moved Permanently

恒久的

GETは変更しない。GET以外は変更される可能性がある。

当初からある恒久的リダイレクトのコード。

302 Found

一時的

GETは変更しない。GET以外は変更される可能性がある。

当初からある一時的リダイレクトのコード。

303 See Other

一時的

GETは変更しない。GET以外はGETに変更する。

302の再定義。POSTリクエストの応答として別のページへGETでアクセスしてほしいケースなどにために作られた。

307 Temporary Redirect

一時的

メソッドも本文も変更しない。GET・HEAD以外はユーザーの確認が必要。

302の再定義。「メソッド変更不可」であることを改めて厳格に定義したもの。

308 Permanent Redirect

恒久的

メソッドも本文も変更しない。GET・HEAD以外はユーザーの確認が必要。

301の再定義。「メソッド変更不可」であることを改めて厳格に定義したもの。

HTTP/1.0(RFC 1945)から定義されていたのは301302だけであった(302は当初「Moved Temporarily」だった)が、302の規格外の使い方(現在の303に相当する使い方)が増えたため、HTTP/1.1(RFC 2068)で303が登場した。さらに、本来メソッドを変えてはいけなかったはずの302を再定義するため、RFC 2616307が登場し、同様に301の再定義となる308RFC 7538で登場した。なお、308以外は現在ではRFC 7231での定義が最新となっている。

GET以外のメソッドが特に問題とならない場合は、現在でも多くの場合で301と302が使われる。POSTやPUTリクエストの結果として、別ページへ移動する必要がある場合(リダイレクト先のURLではなく、最初のURLでPOSTやPUTに対応する処理が行なわれる場合)は303を使う。また、GET以外のメソッドが使われる場合で、それらも完全なリダイレクトが必要となるときは厳格な307と308を使用する。

リダイレクト時のレスポンス

リダイレクト時のレスポンスの例を以下に示す。

  • ステータスコードが300番台になっている。

  • リダイレクト先を表すLocationヘッダが必ず付いている。

  • リダイレクト先が書かれた簡単なHTMLがレスポンスボディに入れられることがある。これは多くの場合、サーバーが自動的に生成する形式的なもので、ブラウザで実際に表示されることはない。

HTTP/1.1 302 Found
Date: Mon, 01 Mar 2021 12:34:56 GMT
Content-Type: text/html; charset=iso-8859-1
Content-Length: 248
Location: https://example.com/hoge/fuga/index.html

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>302 Found</title>
</head><body>
<h1>Found</h1>
<p>The document has moved <a href="https://example.com/hoge/fuga/index.html">here</a>.</p>
</body></html>

Apacheでのリダイレクトの方法

Redirectディレクティブ

Apacheで通常の単純なリダイレクトを行なうには、mod_aliasRedirectディレクティブを使う。

# Redirect ステータスコード 対象パス リダイレクト先
Redirect permanent /aaa/index.html /bbb/index.html

対象パスは必ず「/」から始まる絶対パスを指定する。リダイレクト先は「https://」などから始まるURLか、「/」から始まる絶対パスを指定する。

ステータスコードの部分は、ステータスコードを直接指定する他に、以下のような指定ができる。

キーワード

意味

permanent

301リダイレクト

tempまたは指定なし

302リダイレクト

seeother

303リダイレクト

gone

410レスポンス(Gone)

300番台以外のステータスコードとなる場合は、最後のリダイレクト先は指定できない。

Redirectディレクティブは、実は先頭からパス区切り単位でのマッチングを行なっている。つまり、対象パスとリダイレクト先に、ファイルの代わりにディレクトリを指定すれば、それ以下のファイルが全て1対1にリダイレクトされる。

Redirect permanent /aaa /bbb
# /aaa/ は /bbb/ へリダイレクトされる。
# /aaa/hoge/index.html は /bbb/hoge/index.html へリダイレクトされる。
# /aaa.html や /aaa123/ はリダイレクトされない(パス区切りでマッチしないため)。

トリッキーな例として、リダイレクト元に/index.htmlのようにファイルを指定したつもりでも、仮に/index.html/というディレクトリがあれば、それ以下のファイルもリダイレクト対象となってしまう。また、リダイレクト先を/index.htmlのようにしても、マッチしたパス全てがそのファイルにリダイレクトされるわけではなく、マッチしたパスに応じて/index.htmlの後ろにパスが付加されることになる。

Redirect permanent /index.html /bbb/index.html
# もし /index.html/hoge/ というパスがあれば、 /bbb/index.html/hoge/ にリダイレクトされる。

Redirect permanent /aaa/ /bbb/index.html
# /aaa/hoge/ は /bbb/index.htmlhoge/ にリダイレクトされてしまう。

クエリ文字列は、基本的にはそのまま引き継がれるが、リダイレクト先にあらかじめクエリ文字列を指定した場合は、元のクエリ文字列は無視される。なお、リダイレクト元としてクエリ文字列を指定することはできない。

Redirect permanent /aaa/index.html /bbb/index.html
# /aaa/index.html?hoge=fuga は /bbb/index.html?hoge=fuga にリダイレクトされる。

Redirect permanent /ccc/index.html /ddd/index.html?piyo=paco
# /ccc/index.html?hoge=fuga は /ddd/index.html?piyo=paco にリダイレクトされる。

2つ以上にマッチする場合、最初に記述してあるものが有効となる。サブディレクトリのリダイレクトを記述する場合などは注意が必要となる。

Redirect permanent /aaa/ /bbb/
Redirect permanent /aaa/hoge/ /ccc/
# /aaa/hoge/index.html は /ccc/index.html ではなく /bbb/hoge/index.html にリダイレクトされてしまう。

Redirect permanent /aaa/hoge/ /ccc/
Redirect permanent /aaa/ /bbb/
# /aaa/hoge/index.html が /ccc/index.html に正しくリダイレクトされる。

.htaccessに記述する場合、当然そのディレクトリ以下のパスについてしか設定できない。ただし、その場合でも対象パスは「/」から絶対パスで記述する必要がある。

RedirectMatchディレクティブ

RedirectMatchディレクティブは、Redirectと似ているが、マッチングに正規表現が使用でき、Redirectではできなかった柔軟なリダイレクトが可能となる。

RedirectMatch permanent ^/aaa/.* /bbb/
# /aaa/ 以下全てが /bbb/ にリダイレクトされる。

RedirectMatch permanent ^/ccc/(.+\.jpg)$ /ddd/$1
# /ccc/ 以下の.jpgファイルのみリダイレクトさせる。

RedirectMatch permanent ^/eee/(.+)\.jpg$ /eee/$1.png
# .jpgファイルを.pngファイルにリダイレクトさせる。

Redirectディレクティブと異なり、単純な前方一致ではなく、正規表現でのマッチングとなるため、^(先頭)や$(末尾)の指定を忘れると想定外の結果となる場合があるので注意。

mod_rewrite

さらに細かなリダイレクトを設定するには、mod_rewriteを使う。もう間違えないmod_rewriteを参照。

リダイレクト先URLの取得

JavaScriptで取得

ブラウザ上のJavaScriptだけで、任意のURLのリダイレクト先URLを取得するのは難しい。XMLHttpRequestresponseURLを参照すれば、最終的にたどり着いたURLだけは取得できるが、多段階にリダイレクトしている場合、途中のURLは分からない。また、Ajaxである以上、他のドメイン(オリジン)のURLを調べることは原則としてできない。

const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://example.com/');
xhr.onload = function() {
    console.log(xhr.responseURL);
};
xhr.send();

curlコマンドで取得

HTTPのレスポンスを細かく調べるには、どうしてもブラウザ上の制限から抜け出す必要がある。最も簡単なのは、Linuxなどで使用できるcurlコマンドを使う方法である。

# 最初のURLのみ
curl -s -I https://example.com/ | grep -i "Location:"

# 最終的なリダイレクト先まで全て
curl -s -I -L https://example.com/ | grep -i "Location:"

curlコマンドの-Iオプションでレスポンスヘッダのみを出力し、その中からリダイレクト先が書かれているLocationヘッダをgrepで取り出している。-Lオプションをつけると、リダイレクト先に自動的にリクエストを再送信してくれるため、多段階にリダイレクトされている場合でも全てのURLを取得できる。-sは詳細情報を出力しないオプション、grepの-iは大文字小文字を無視するオプションである。

wgetコマンドで取得

wgetコマンドでもレスポンスヘッダを取得することができる。

# 最初のURLのみ
wget -q -S --max-redirect=0 https://example.com/ 2>&1 | grep -i "Location:"

# 最終的なリダイレクト先まで全て
wget -q -S https://example.com/ 2>&1 | grep -i "Location:"

wgetコマンドでは-Sオプションでレスポンスヘッダを標準エラーに出力できる。-qは詳細情報を出力しないオプションである。wgetの場合、自動でリダイレクト先に再リクエストしてくれるため、最初のURLのみ取得する場合は、--max-redirect=0でリダイレクト回数を制限すればよい。

Node.jsで取得

ブラウザ上のJavaScriptでは難しかったが、Node.jsであればrequestモジュールでHTTPリクエストを投げてそのレスポンスを調べることができる。

const request = require('request');
request({
    uri: 'https://example.com/',
    followRedirect: false
}, function(err, res, body) {
    console.log(res.headers.location);
});

ただ、requestモジュールは2021年現在deprecatedとなってしまっているため、今後は別のモジュールを使っていく必要がある。例えば、axiosモジュールを使う場合、以下のようになる。

const axios = require('axios');
axios.get('https://example.com/', { maxRedirects: 0 }).catch(function(res) {
    console.log(res.response.headers.location);
});

Perlで取得

PerlではLWP::UserAgentを使う。ただ、場合によっては、curlコマンドやwgetコマンドを直接呼んでしまう方が簡単である。

use LWP::UserAgent;
my $ua = LWP::UserAgent->new(max_redirect => 0);
my $res = $ua->get('https://example.com/');
print $res->header('location') . "\n";