Dev Study
← HTTPとTLSの仕組み
レッスン 3 / 12·約 8 分·第18期 HTTPとTLSの仕組み

リダイレクトは、依頼をクライアントに作り直させる

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力今ここ
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口今ここ
  6. アプリ処理
  7. DB保存

前回は、知らない値の扱いを両端が先に共有していることを見ました。今回は、リダイレクトの後の依頼を誰がどの規則で作るのかを見ます。

まず結論

リダイレクトでサーバが返すのは、行き先とステータスコードだけです。2回目の依頼を組み立てて送るのはクライアントで、そのときメソッドを変えるかどうかの規則は、長いあいだ両端で共有されていませんでした。

301/302 で POST が GET になりうるのも、307 と 308 が後から足されたのも、その規則をコードの番号そのものに持たせて共有し直した経緯です。

? 読む前に予測

`curl -s -L -d x=1` で POST を送り、サーバが 302 と `Location: /dest` を返しました。curl が次に `/dest` へ送る依頼はどれでしょうか(macOS 同梱の curl 8.7.1 で実測)。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

サーバは行き先を言うだけで、依頼を作るのはクライアント

このコースの芯は、通信の仕組みは両端が何を共有しているかで決まる、というものです。共有していれば省略でき、共有していなければ往復が要ります。

リダイレクトはわかりやすい往復です。サーバは「そこではなく、あちら」とだけ返し、クライアントがもう一度依頼を送ります。問題は、その2回目の依頼を誰が、どの規則で組み立てるかです。RFC 9110 §15.4 はこう書きます。

When automatically following a redirected request, the user agent SHOULD resend the original request message with the following modifications:

続く箇条には、宛先を Location の URI に差し替える、Change the request method according to the redirecting status code's semantics, if applicable.、そして If the request method has been changed to GET or HEAD, remove content-specific header fields が並びます。

作り直すのはクライアントです。サーバは、元の本文を持ち直してもいませんし、2回目を代わりに送ってもいません。だからメソッドを変えるかどうかの規則を両端が共有していないと、サーバが意図した依頼は届きません。

リダイレクトで、2回目の依頼を組み立てる側
クライアントPOST /r/308 本文 x=1
サーバが返すのはこれだけ
サーバ308 と Location: /dest
ここはクライアントの中で起きる
宛先を差し替える/r/308 → /dest
メソッドを決めるコードの意味に従う
本文を付けるか決めるGET に変えたら落とす
作り直した依頼
サーバPOST /dest 本文 x=1
  • ・メソッドと本文の扱いは、ステータスコードの番号とクライアントの実装で決まる
  • ・サーバ側が選べるのは、コードと Location
具体実物

5つのコードに同じ POST を送る

/r/NNN に来た依頼には Location: /dest を付けてコード NNN を返し、届いた依頼をすべて記録する Node のサーバです(Node v24.21.0)。

const http = require('node:http');
http.createServer((req, res) => {
  let body = '';
  req.on('data', c => body += c);
  req.on('end', () => {
    console.log(`${req.method} ${req.url} content-length=${req.headers['content-length'] ?? '-'} body=${JSON.stringify(body)}`);
    const m = req.url.match(/^\/r\/(\d{3})$/);
    if (m) { res.writeHead(Number(m[1]), { location: '/dest' }); return res.end(); }
    res.writeHead(200, { 'content-type': 'text/plain' });
    res.end(`dest got ${req.method}\n`);
  });
}).listen(Number(process.argv[2] || 18083), () => console.log('listening', process.version));

macOS の curl 8.7.1 で、コードごとに curl -s -L -d x=1 http://127.0.0.1:18083/r/NNN を送りました。2回実行して同じ結果です。

サーバ側のログ(1回目の実行、`-d x=1` だけ。生出力の該当10行)コマンド出力
POST /r/301 content-length=3 body="x=1"
GET /dest content-length=- body=""
POST /r/302 content-length=3 body="x=1"
GET /dest content-length=- body=""
POST /r/303 content-length=3 body="x=1"
GET /dest content-length=- body=""
POST /r/307 content-length=3 body="x=1"
POST /dest content-length=3 body="x=1"
POST /r/308 content-length=3 body="x=1"
POST /dest content-length=3 body="x=1"

301・302・303 の後は GET で本文なし、強調した 307・308 の後だけ POST で本文 x=1 が送り直されています。curl のマニュアルの -L の項が、この規則をそのまま書いています。

When curl follows a redirect and if the request is a POST, it sends the following request with a GET if the HTTP response was 301, 302, or 303. If the response code was any other 3xx code, curl resends the following request using the same unmodified method.

これは curl の規則です。ブラウザが同じ動きをするかは、ここでは確かめていません。

✓ ここまでの確認

307 を返した後、`/dest` に本文 x=1 付きの POST が届きました。この2回目の依頼を組み立てて送ったのは誰ですか。

抽象原理

規則が共有されていなかったから、コードが増えた

なぜ「恒久的に移った」が 301 と 308、「一時的に移った」が 302 と 307 の2つずつあるのでしょうか。RFC 9110 §15.4 の Note が経緯をまとめています。

However, early user agents split on whether to redirect POST requests as POST (according to then-current specification) or as GET (the safer alternative when redirected to a different site). Prevailing practice eventually converged on changing the method to GET.

HTTP/1.0(RFC 1945)は、POST への 301・302 をユーザの確認なしに自動で追うことを認めていませんでした。そのうえで 301 と 302 の両方に some existing user agents will erroneously change it into a GET request. と Note を付けています。仕様と実装の間で、作り直しの規則が割れていたわけです。

その後の対処は、規則を番号に埋め込むことでした。

作り直しの規則が番号に移っていった経緯(日付は各 RFC のヘッダ。7238 の月は RFC 9110 の Note による)表
文書 定めたこと
RFC 1945 (1996-05) 301・302。POST の自動転送は確認なしでは不可
RFC 2068 (1997-01) 303 を追加。GET で取りに行かせる
RFC 2616 (1999-06) 307 を追加。一時的な移転でメソッドを保つ
RFC 7238 (2014-06) / 7538 308 を追加。恒久的な移転でメソッドを保つ
RFC 9110 (2022-06) 301・302 の POST→GET を MAY で追認

RFC 2616 は 302 の Note で most existing user agent implementations treat 302 as if it were a 303 response と実態を認めています。RFC 7538 は、恒久版の欠けを埋めるものとして 308 を足しました。RFC 9110 は 301・302 に For historical reasons, a user agent MAY change the request method from POST to GET と書き、変えてほしくなければ 308・307 を使えと続けます。

303・307・308 は、メソッドをどう扱うかを番号だけで伝えられるように足されたコードです。

具体実物

語を上書きしても、作り直しの規則は変わらない

では、クライアントに「POST のままで」と外から言えば済むでしょうか。同じサーバに curl -s -L -X POST -d x=1 を送りました。

サーバ側のログ(1回目の実行、`-X POST -d x=1`。生出力の該当10行)コマンド出力
POST /r/301 content-length=3 body="x=1"
POST /dest content-length=- body=""
POST /r/302 content-length=3 body="x=1"
POST /dest content-length=- body=""
POST /r/303 content-length=3 body="x=1"
POST /dest content-length=- body=""
POST /r/307 content-length=3 body="x=1"
POST /dest content-length=3 body="x=1"
POST /r/308 content-length=3 body="x=1"
POST /dest content-length=3 body="x=1"

5つとも2回目の語は POST になりました。しかし強調した 301・302・303 の行には本文がありません。curl のマニュアルは -X を This option only changes the actual word used in the HTTP request, it does not alter the way curl behaves. と説明します。本文を落とす作り直しはそのまま動き、行頭の語だけが差し替わったのです。

サーバの側から「POST のまま」を伝えたいなら、返すコードそのものを変える必要があります。

✓ ここまでの確認

`curl -s -L -X POST -d x=1` で 303 を受けたとき、`/dest` に届いた依頼として実測と合うのはどれですか。

⇄ 2つを見比べる
具体例 A

308 を返す(curl 8.7.1 で実測)

サーバはコードと Location だけを返し、curl が宛先を差し替えて POST と本文を送り直しました。メソッドを保つことは 308 という番号で伝わっています。往復が1回増え、クライアントは新しい URI を知ります。

サーバ側のログ(該当2行)コマンド出力
POST /r/308 content-length=3 body="x=1"
POST /dest content-length=3 body="x=1"
具体例 B

nginx の proxy_pass(「Nginx設定を書く — proxy_pass とスラッシュ」)

nginx が上流へ代わりに取りに行き、クライアントは何も作り直しません。あのレッスンの比較の1つ目で見た URI の置換は、nginx が上流へ送る依頼を自分で組み立てるときの規則でした。一方、同じ nginx が末尾スラッシュ抜きの URI には自分で 301 を返す場合があることを、全体地図のレッスンで見ています。そちらではクライアントが作り直します。

どちらも「別の場所から取ってくる」仕組みです。何を共有していなければならないかは、どこで分かれるでしょうか。

2回目の依頼を組み立てているのが、どちらの端かを見てください。

この章のまとめ

  • リダイレクトでサーバが返すのはコードと Location だけ。2回目の依頼を組み立てるのはクライアント(RFC 9110 §15.4 SHOULD resend the original request message with the following modifications)。
  • curl 8.7.1 の実測では、POST に対して 301・302・303 の後は GET で本文なし、307・308 の後は POST で本文ごと送り直した。
  • HTTP/1.0 では、POST の確認なしの自動転送は認められず、GET に変える実装は erroneously と書かれていた。規則が共有されていなかった。
  • 303(RFC 2068)、307(RFC 2616)、308(RFC 7238/7538)は、メソッドの扱いを番号で伝えるために足された。303 を HTTP/1.0 由来と書かない。
  • RFC 9110 は 301・302 の POST→GET を歴史的理由による MAY として追認している。仕様違反とも、必ず GET になるとも言えない。
  • curl -X POST は語だけを変える。301・302・303 では本文が落ちた POST になる。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

環境:

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 3正解: 0

303 (See Other) が最初に定義された文書はどれですか。

用語集(3語)
リダイレクト (3xx)
別の URI へ行くよう促す応答。RFC 9110 §15.4 では、自動で追う場合に元の依頼を作り直して送るのはユーザエージェント側。
303 See Other
処理結果を別のリソースとして取りに行かせるコード。取りに行く依頼は GET か HEAD。初出は RFC 2068。
307 / 308
メソッドを保つ前提の一時/恒久リダイレクト。307 は RFC 2616、308 は RFC 7238(2014年6月)と RFC 7538 で追加され、現在は RFC 9110 が定める。