3xx リダイレクト画面のマークアップ
HTTP レスポンスのステータスコードのうち、300番台の一部では Location ヘッダーフィールドを併用することでリダイレクトを設定することができます。この場合ブラウザでは通常リダイレクト画面は表示されないのですが、だからといってレスポンスコンテンツ(ボディ)が不要なわけではなく、通常の画面(200 OK)と同様に正しいマークアップを心掛ける必要があります。
ところが世の中の Web サーバーや Web フレームワークは不正な HTML を返すものも多く、リダイレクトが行われなかった場合(リダイレクト画面が表示された場合)にユーザーが不利益を被っているのが実情です。本記事ではなぜリダイレクト画面のマークアップにもこだわる必要があるのか、仕様とブラウザ挙動の両面から考えてみます。
RFC の記述
リダイレクトの挙動
RFC 9110 の 15.4. Redirection 3xx(www.rfc-editor.org) では、300, 301, 302, 307, 308 のステータスコードについて The user agent MAY use the Location field value for automatic redirection
と書かれています。MUST や SHOULD でなく MAY であることに注目してください。
なので Location ヘッダーフィールドが設定されていようとも自動リダイレクトしないブラウザがあっても良いのです。これは「RFC でそう定められているから」という理由はもちろんですが、それ以前に Web の考え方として本来すべての制御権はユーザーにあるべきです。多くのユーザーにとっては自動でリダイレクトされたほうが便利な一方、プライバシーその他の理由であえてそれを無効にする自由は保障されなければなりません。
レスポンスコンテンツ(ボディ)
必ずしも自動リダイレクトされなくてもよいということは、すなわち Web 制作者はリダイレクトされないケースを想定し、300番台のステータスコードと Location ヘッダーフィールドを設定して満足するのではなく、リダイレクト画面においても適切なコンテンツを返す必要があると言えるでしょう。
それについて仕様はどう言っているのか、歴代の RFC の記述を見てゆきましょう。以下、特記のない限り 301 Moved Permanently のケースを挙げてゆきます。まずは RFC 1945(1996年、HTTP/1.0)。
Unless it was a HEAD request, the Entity-Body of the response should contain a short note with a hyperlink to the new URL.
If the 301 status code is received in response to a request using the POST method, the user agent must not automatically redirect the request unless it can be confirmed by the user, since this might change the conditions under which the request was issued.
www.rfc-editor.org)
このように POST リクエストにおいては自動リダイレクトしてはならないとされていました。「ユーザーによる確認(confirmed by the user
)」が具体的にどのようなものかは想像になりますが、確認ダイアログで「OK」を押すまでの間、あるいは「キャンセル」を押した後はレスポンスボディの HTML を画面表示することが想定されていたと仮定すると、a short note with a hyperlink
を含めるべきとされていた理由には納得できます。もっともこのとおりに実装されたブラウザは皆無だったと言われていますから、あくまで机上の空論です。
次に RFC 2068(1997年、HTTP/1.1)。多少表現は変わっているものの大意は同じです。
Unless the request method was HEAD, the entity of the response SHOULD contain a short hypertext note with a hyperlink to the new URI(s).
If the 301 status code is received in response to a request other than GET or HEAD, the user agent MUST NOT automatically redirect the request unless it can be confirmed by the user, since this might change the conditions under which the request was issued.
www.rfc-editor.org)
そしてお馴染み RFC 2616(1999年、HTTP/1.1 改訂)では、301, 302, 303 の記述は RFC 2068 を引き継いでいるものの、新設された 307 では後方互換性の理由付けが足されています。
Unless the request method was HEAD, the entity of the response SHOULD contain a short hypertext note with a hyperlink to the new URI(s) , since many pre-HTTP/1.1 user agents do not understand the 307 status.
www.rfc-editor.org)
15年後に更新された RFC 7231(2014年)では記述が大きく変わり、あくまで「みなさんそのようにされていますね」と現状説明をするに留まっています(パチンコ店かよ)。これは RFC 2616 までに存在した「GET, HEAD 以外の応答時にはユーザーが確認行為を行うまで自動リダイレクトしてはならない」が実情に合わせて撤回されたことに合わせての変更と思われます。
The server's response payload usually contains a short hypertext note with a hyperlink to the new URI(s).
www.rfc-editor.org)
ただし 303 See Other のみは他と大きく異なっています。RFC 2119 のキーワードである SHOULD こそ使用されていないものの、ought to contain
とあることから、RFC 2616 以前に引き続きレスポンスコンテンツにはリンク付きの HTML を含める必要があります。
Except for responses to a HEAD request, the representation of a 303 response ought to contain a short hypertext note with a hyperlink to the same URI reference provided in the Location header field.
www.rfc-editor.org)
同じ2014年には 308 Permanent Redirect が追加され、RFC 7238(www.rfc-editor.org) として発行されています。レスポンスコンテンツの記述は RFC 7231 と同一です(引用は省略)。2015年には RFC 7538(www.rfc-editor.org) に更新されていますが、記述は変わっていません。
最新の RFC 9110(2022年)では用語の変更に伴い多少文章が変わっているものの、大意は変わりません。
The server's response content usually contains a short hypertext note with a hyperlink to the new URI(s).
www.rfc-editor.org)
このように歴代の RFC ではいずれもレスポンスコンテンツにハイパーリンク付きの短いテキストを含めることへの言及があるのですが、その程度感は下表のようにそれぞれ異なります。
| RFC | 301 | 302 | 303 | 307 | 308 |
|---|---|---|---|---|---|
| RFC 1945 | should | should | ― | ― | ― |
| RFC 2068 | SHOULD | SHOULD | SHOULD | ― | ― |
| RFC 2616 | SHOULD | SHOULD | SHOULD | SHOULD | ― |
| RFC 7231 | usually | usually | ought | usually | ― |
| RFC 7238 | ― | ― | ― | ― | usually |
| RFC 7538 | ― | ― | ― | ― | usually |
| RFC 9110 | usually | usually | ought | usually | usually |
すなわち1990年代と比べて現在では程度感が緩和されていると言えるのですが、いくつかの注意点があります。
- 301, 302, 307, 308 ではレスポンスコンテンツを過去には含めるべき(SHOULD)とされていたが、現在では必ずしも含める必要はない。
- 303 は HEAD リクエストへの応答でない限り、レスポンスコンテンツを含めるべき。ただし MUST ではないので含めなかったとしても仕様違反になるわけではない。
- レスポンスコンテンツは短いハイパーテキストであり、なおかつハイパーリンクを含めた方がよいとされている。つまり事実上 HTML 形式であることが求められている。ただし必須条件ではないので API サービスでは XML や JSON 形式で返しても問題はないだろう。
自動リダイレクトしない環境
仕様上、レスポンスコンテンツは必ずしも必要なわけではないことが分かりました。では実際のところ省略しても構わないのでしょうか。ユーザーエージェントの挙動を見てみましょう。
Presto Opera 12.1
まずは古い話ですが Opera のうちバージョン12以前の、いわゆる Presto Opera の例を挙げましょう。
「設定」–「詳細設定」–「ネットワーク」の中にオートリダイレクトを有効にする
のオプションがありました。これをオフにすると Location ヘッダーフィールドあるいは <meta http-equiv="refresh"> 要素による自動リダイレクトを無効にすることができます。利便性を犠牲にしてでもオープンリダイレクトなどの脅威低減を優先したいといった需要が、当時はブラウザに設定項目を設けるほどには存在したのでしょう。
すべてのウェブサイトを UTF-8 でエンコードするなどに並んで
オートリダイレクトを有効にするのチェックボックスがある。
これだけ見ると理屈は分かるが完全にオフにするのは現実的ではないと思われるでしょうが、実際には「コンテンツ」–「サイトごとの設定を編集」においてドメインごとに有効/無効を切り替えることが可能でした。つまり信頼できるドメインではリダイレクトを許可したり、逆に脆弱性が放置されている危険なドメインでは不許可にしたりといった運用ができたのです。これは充分に現実的なもので、実際私も当時(2010年代初頭まで)はそのように運用していました。
これらの設定を行うと、リダイレクト画面(3XX に限らず <meta> 要素によるリダイレクトを含む)でもリダイレクト先への自動移動は行われず、レスポンスコンテンツの内容が表示されます。
Android Firefox 146
Android Firefox ではアプリ連携された URL に Location ヘッダーフィールドでリダイレクトすると、引き続きブラウザで閲覧するのではなく当該アプリに移動することもできるのですが、そうするとブラウザでは 3xx 画面のレスポンスコンテンツが表示された状態になる事象が昔から存在します。
Bluesky を例にすると以下の手順で再現できます(アプリとドメインが紐付けられるものなら他のアプリでも同様です)。
- Android 端末に Firefox(
play.google.com) と Bluesky(play.google.com) のアプリをインストール - Firefox で
Location: https://bsky.app/を返すリダイレクト画面にアクセス アプリで開く
の選択ダイアログが出現するので「Bluesky」を選択- Blusky のアプリが立ち上がる(裏では引き続き Firefox が開かれている)
- Blusky のアプリを閉じる
- ふたたび Firefox に戻るが、普通は見えないはずのリダイレクト画面が表示されている
Moved Permanentlyの見出しに続いてThe document has moved here.の文章が表示されている。この挙動は Mozilla が意図したものかそれともソフトウェアのバグなのかは分かりませんが、仮にバグだとしても「リダイレクト画面が見えること」そのものは前述の RFC の記述にあるように Web の仕組み上あり得ることです。Presto Opera のサポートが終了した現代においても、Web 制作者はリダイレクト画面が見える可能性を考慮しなければならないことが実体験できる良い教材と言えるでしょう。
ブラウザ以外
Web コンテンツにアクセスするのはブラウザだけではありません。それらの中には自動リダイレクトしないことがデフォルト動作なものも存在します。
たとえば curl は -L オプション(curl.se)を指定しない限りリダイレクトが反映されません。このブログのトップページの URL を https: でなくあえて http: で指定するとこうなります。
user@hostname:~$ curl http://blog.w0s.jp
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>301 Moved Permanently</title>
</head><body>
<h1>Moved Permanently</h1>
<p>The document has moved <a href="https://blog.w0s.jp/">here</a>.</p>
<hr>
<address>Apache/2.4.63 (Ubuntu) Server at blog.w0s.jp Port 80</address>
</body></html>
リダイレクトが行われずともレスポンスコンテンツの HTML 内で「コンテンツが移動したこと」「移動先の URL」といった情報が提示されているので、ユーザーには意図が伝わるでしょう。
Web サーバーの例
ユーザーエージェントの環境によっては自動リダイレクトが行われなかったり、あるいはリダイレクトの有無に関わらず「リダイレクト画面が見える」状況が発生することが分かりました。それでは Web サーバー側はそのような状況に対応できているのでしょうか。
著名な Web サーバーとして、Apache HTTP Server と nginx の最新版で 301 Moved Permanently を発生させた例を以下に示します。
Apache httpd 2.4.66
# conf/httpd.conf
Redirect permanent / https://example.com/
HTTP/1.1 301 Moved Permanently
Date: XXX
Server: Apache/2.4.66 (Win64)
Location: https://example.com/
Content-Length: 268
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=iso-8859-1
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html><head>
<title>301 Moved Permanently</title>
</head><body>
<h1>Moved Permanently</h1>
<p>The document has moved <a href="https://example.com/">here</a>.</p>
</body></html>
ISO-8859-1 だったり HTML 4.01 だったりと古臭さは否めませんが、実用上の問題はないでしょう。一般論として here 症候群は避けるべきですが、リダイレクト画面においてはほかに方法はなく、やむを得ないことと思います。
強いて現代的な視点から難点を挙げるならば <html> 要素の開始タグに lang 属性を付けたり、<meta> 要素で Viewport の指定を入れたりしたいところですが、この規模の HTML のことなので現状でも大きな不便は生じないはずです。
ちなみに Apache HTTP Server のリダイレクト画面が HTML 4.01 に“進化”したのはここ最近のことで、少し前までのバージョンでは HTML 2.0 でした。なぜ HTML5 にせず HTML 4.01 に留まったのかは気になるところです。
nginx 1.29.4
# conf/nginx.conf
location / {
rewrite ^ https://example.com/ permanent;
}
HTTP/1.1 301 Moved Permanently
Server: nginx/1.29.4
Date: XXX
Content-Type: text/html
Content-Length: 169
Connection: keep-alive
Location: https://example.com/
<html>
<head><title>301 Moved Permanently</title></head>
<body>
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx/1.29.4</center>
</body>
</html>
Apache より後発のソフトウェアであるにも関わらず、見てのとおり残念な HTML です。DOCTYPE がないとか、廃止された <center> 要素を使っているといった点は、先月(2025年12月)になって修正の Pull Request が投げられているので、近いうちに解消されそうな感があります。
しかしリダイレクト先の URL がレスポンスコンテンツに現れていないのは問題があるでしょう。Issue #1083 を立てており、現在はメンテナーの反応待ちです。
Web フレームワークの例
現代では Web サーバーで直接コンテンツを配信するのではなく、フレームワークがそれを担当するケースも多くなっています。
著名な Web フレームワークとして、Express と Hono の最新版で 301 Moved Permanently を発生させた例を以下に示します。
Express 5.2.1
Node.js のフレームワークである Express では res.redirect() メソッド(expressjs.com)で 3xx リダイレクトを設定することが可能です。しかしこの機能を使用した 3xx 画面では不正な HTML が出力されてしまいます。具体的には MIME タイプが text/html、つまり HTML 画面であるにも関わらず <title> 要素がないのです。
import express from 'express';
const app = express();
app.get('/', (req, res) => {
res.redirect(301, 'https://example.com/');
});
app.listen(3000);
HTTP/1.1 301 Moved Permanently
X-Powered-By: Express
Location: https://example.com/
Vary: Accept
Content-Type: text/html; charset=utf-8
Content-Length: 61
Date: XXX
Connection: keep-alive
Keep-Alive: timeout=5
<p>Moved Permanently. Redirecting to https://example.com/</p>
2022年に DOCTYPE と <title> 要素を含めるべきという旨の Issue を提出しており、先日ようやく Pull Request
がマージされた次第です。まだリリースはされていないのですが、近いうちに解消されるでしょう。
一方、URL に <a href> によるリンクが設定されていないことに注目してください。もともとはリンクがあったのですが、2024年9月リリースのバージョン 4.20.0 にてどういうわけか外されてしまったのです。Issue で議論された形跡がなく、変更の理由が分からないのですが、これ以来自動リダイレクトが行われない環境ではユーザーは URL を手動でコピペする必要に迫られています。何かしらの要因でリンクができないにしろ、せめて URL 部分は
<code> 要素や <data> 要素でマークアップするなど、セマンティックな情報提示を行ってほしいところですが……。
これらの問題に対して、Express の利用者(Web 制作者)は res.send() メソッド(expressjs.com)を利用することで独自の HTML を返すようにカスタマイズが可能です。
import { escape } from '...';
app.get('/', (req, res) => {
const redirectPath = 'https://example.com/';
res.status(301).location(redirectPath).send(`
<!DOCTYPE html>
<html lang=ja>
<meta name=viewport content="width=device-width,initial-scale=1">
<title>ページ移動</title>
<p>このページは <a href="${escape(redirectPath)}"><code>${escape(redirectPath)}</code></a> に移動しました。`);
});
そもそも日本語サイトならリダイレクト画面も日本語で提供するのが好ましいので、フレームワークデフォルトの機能に頼るのではなく、独自にカスタマイズするのが望ましいでしょう。
Hono 4.11.4 + @hono/node-server 1.19.9
Hono は本ブログで使用しているフレームワークですが、Express と似た状況です。c.redirect() メソッド(hono.dev)が用意されているのですが、ユーザーはそれに頼るべきではありません。
import { Hono } from 'hono';
import { serve } from '@hono/node-server';
const app = new Hono();
app.get('/', async (c) => {
return c.redirect('https://example.com/', 301);
});
serve(app);
export default app;
HTTP/1.1 301 Moved Permanently
location: https://example.com/
content-type: text/plain; charset=UTF-8
Date: XXX
Connection: keep-alive
Keep-Alive: timeout=5
Content-Length: 0
Content-Length: 0 が哀愁を誘いますね。これは 301 の例ですが、RFC でレスポンスコンテンツを含めることが求められている 303 も同じ状況です。c.html() メソッド(hono.dev)を使いましょう。
import { escape } from '...';
app.get('/', async (c) => {
const redirectPath = 'https://example.com/';
return c.html(
`
<!DOCTYPE html>
<html lang=ja>
<meta name=viewport content="width=device-width,initial-scale=1">
<title>ページ移動</title>
<p>このページは <a href="${escape(redirectPath)}"><code>${escape(redirectPath)}</code></a> に移動しました。`,
301,
{ Location: redirectPath },
);
});
Apache HTTP Server(1995年)、nginx(2004年)、Express(2010年)、Hono(2021年)の4つの事例を見てきましたが、いちばんまともな HTML を提供しているのが Apache HTTP Server であり、新興のソフトウェアになるにつれて状況が悪化してゆくのは果たして偶然でしょうか。個人の肌感覚ではありますが、1990年代〜2000年代初頭までは「自動リダイレクトが行われない」環境に対する嗅覚が敏感であったのに対し、現代ではそのようなことを考慮するエンジニアが少なくなってしまった気がしています。
幸いにというべきか、フレームワークには大抵任意の HTML を返す機能が用意されているので、可能な限りそれを利用しましょう。

