ブラウザのPOSTリクエストは、リダイレクトさせるとGETに化ける?

POSTがGETになってる?

前回のエントリーにもつながっている話ですが、ブラウザからフォームでPOSTされたものがリダイレクトされたときの挙動について。挙動が理解できると、化けるってのも失礼な表現なんですが。以下、化けた理由と、HTTP周りの実装の話をつらつらと。

普通のHTMLなフォームからPOST

<form action="post_receiver" method="post" enctype="multipart/form-data">
    <input type="file" name="imageFile" />
    <input type="submit" value="画像をアップロードしてやんよ" />
</form>

上記のようなHTMLなフォームから、POSTした際にサーバー側でうまくPOSTデータを受け取れないことがありました。

echo $_SERVER['REQUEST_METHOD']; // GET

なんでだろー?と思って、PHP側で、$_SERVER変数をダンプしてみたら、なぜかリクエストがGETに変質していました。

Live HTTP Headersでリクエストとレスポンスを追跡

お馴染みのLive HTTP Headers :: Add-ons for Firefoxで、リクエストとレスポンスを追跡してみました。

まずはフォームからPOSTすると...

普通にPOSTでリクエストが飛んでいます。

http://example.com/post_receiver

POST /post_receiver HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8,application/json
Accept-Language: ja,en-us;q=0.7,en;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 115
Connection: keep-alive
Content-Type: multipart/form-data; boundary=---------------------------184868242010120281441086531968
Content-Length: 231
-----------------------------184868242010120281441086531968
Content-Disposition: form-data; name="imageFile"; filename=""
Content-Type: application/octet-stream

-----------------------------184868242010120281441086531968--

301リダイレクトされた

リクエストされたパスの末尾にスラッシュを加えて、URLを正規化するときのリダイレクト。mod_rewriteの動作ですね。

HTTP/1.1 301 Moved Permanently
Date: Tue, 09 Nov 2010 07:12:42 GMT
Server: Apache
Location: http://example.com/post_receiver/
Vary: Accept-Encoding
Content-Encoding: gzip
Content-Length: 197
Connection: close
Content-Type: text/html; charset=iso-8859-1

素直にリダイレクトするわけですが

ここでリクエストがGETになっています。POSTデータも消滅。ぎゃー。

http://example.com/post_receiver/

GET /post_receiver/ HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8,application/json
Accept-Language: ja,en-us;q=0.7,en;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 115
Connection: keep-alive
Referer: http://example.com/

HTTP/1.1 200 OK
Date: Tue, 09 Nov 2010 07:31:52 GMT
Server: Apache
X-Powered-By: PHP/5.2.14
Vary: Accept-Encoding,User-Agent
Content-Encoding: gzip
Content-Length: 22
Connection: close
Content-Type: text/html; charset=UTF-8

301, 302, 303, 307のステータスコードが返ると、ブラウザはGETでリダイレクトする

つまり、そういうことらしいです。確かに、ユーザーからすれば何処にリダイレクトされたかも分からないようなリソースに、POSTデータを勝手に送ってしまっては少々お行儀が悪いですね。

上の参考エントリー内でも言及されていますが、HTTPステータスコードの詳細を見てみると、確認さえすれば元のメソッドでリダイレクトすべきものもあります。何でもかんでも暗黙裏にGETメソッドでリダイレクトさせてしまう現実のブラウザは、本来の仕様とは異なった実装がされているということになります。

リダイレクト系のステータスコードについて

301 Moved Permanently
恒久的な現在のリソースのURIを示す。
302 Found ( Moved Temporarily )
一時的な現在のリソースのURIを示す。GET・HEAD以外のリクエストのレスポンスとして受信した際、ユーザーの確認なしにリクエストをリダイレクトしてはならないし、リクエストメソッドを変更してもならない。
303 See Other
リクエストの後に、GETで回収すべきリソースのURIを示す。update.phpにPOSTした後に、content.htmlをGETして欲しい時などに使う。
307 Temporary Redirect
302と同様の意味をもつ。リダイレクトのポリシーも同じ。

元々、301と302があったわけですが、昔から302が本来の意図を上回って色々な使われ方をしてきました。一部の使い方は、不適切な使い方とされ、302の本来の意味がぼやけてしまいました。

そこで、HTTP1.1で新しいステータスコードが作られ、302で解決すべきでなかった用途を新たに満たす「303」と、302の本来の意味を明示的に分離させた「307」が追加されましたという経緯らしいです。

ステータスコードを新設したところで、これまでに積み上げられてきたシステムは、ブラウザの302周りの誤った実装に依存しているものも多いでしょうから、ブラウザ側もそう簡単には実装を変えられない、って事情でしょうか。それならそれで、せめて307は仕様通りの実装にすべきだと思いますが。

POST先でのリダイレクトには気を遣おう!

基本的にPOSTリクエストをリダイレクトで、どこかに受け流すことはできないし、mod_rewriteでうっかりリダイレクトされる時も気をつけたい。今回の自分みたく、末尾のスラッシュをつけるためのリダイレクトの時とか...。正しい実装という観点からすれば、303や307もちゃんと使い分けて送出するようにしたいですね。

ではでは。

参考URL