理系学生日記

おまえはいつまで学生気分なのか

忍者TOOLS

POST をリダイレクトすると GET になる件について調べた

とある事情により、POST リクエストをリダイレクトさせる必要が生じました。単純にリダイレクトさせてみたところ、リダイレクトはされるものの、POST リクエストに付与していた HTTP_BODY が取得できません。どうも、リダイレクト時に GET に変更されているみたいです。 ぼくは怒りに震えたものの、RFC 的にはどう振る舞うべきなんだ、各種ブラウザの振舞いはどうなっているんだ、ということが気になったのでまとめてみました。内容としては、 -POSTリクエストをリダイレクトするとGETされる?POSTされる? - はこべにっき ♨ の二番煎じになります。

先に結果を示しておくと、以下のとおりでした。 |Status Code|期待動作|Firefox (25.0.1)|Safari(7.0)|*Chrome (31.0)| |301|POST|GET|GET|GET| |302|POST|GET|GET|GET| |303|GET|GET|GET|GET| |307|POST|POST|POST|POST|

まずは RFC からリダイレクトの整理 まずここでリダイレクトを行うステータスコードについて整理し、さらに、RFC の注釈を引用します。この注釈を読むことで、RFC 側の苦悩が読み取れます。 |Status Code|Reason Phrase|*意味合い| |301|Moved Permanently|リソースの恒久的な移動。できるんだったら参照先を変更しろ。| |302|Found|リソースの一時的な移動。移動先が変更されるかもしれないから、キャッシュとかすんな。| |303|See Other|リクエストに対するレスポンスは他の URI にあるから GET メソッドで取得しろ。| |307|Temporary Redirect|302 と同じ。ただ、pre-HTTP/1.1 なクライアントは理解できないかもな。|

まず、301 (Moved Permanently) について。

Note: When automatically redirecting a POST request after receiving a 301 status code, some existing HTTP/1.0 user agents will erroneously change it into a GET request.

訳) 301 のステータスコードを受けとって POST リクエストを自動でリダイレクトする際、既存の HTTP/1.0 ユーザエージェントの中には誤って GET リクエストに変更しちゃうものがある << 文章からすれば、301 のステータスコードによる POST リクエストのリダイレクトは、GET メソッドに変更しちゃダメという解釈になります。付け加えるとするならば、

The action required MAY be carried out by the user agent without interaction with the user if and only if the method used in the second request is GET or HEAD.

訳) 要求されるアクションは、(リダイレクト時に使用される)2 回目のメソッドが GET か HEAD の場合にのみ、ユーザとのインタラクションなしで実行されても良いものとする << ということなので、POST のままリダイレクトする場合は、ユーザにその旨を伝えろ、というのが RFC 的な推奨になるでしょうか。

続いて 302 (Found)。

Note: RFC 1945 and RFC 2068 specify that the client is not allowed to change the method on the redirected request. However, most existing user agent implementations treat 302 as if it were a 303 response, performing a GET on the Location field-value regardless of the original request method. The status codes 303 and 307 have been added for servers that wish to make unambiguously clear which kind of reaction is expected of the client.

訳) RFC 1945 と RFC 2068 では、リダイレクト時のリクエストにおけるメソッドの変更は許されてないとしている。にも関わらず、ほとんどの既存ユーザエージェントの実装は、302 をあたかも 303 のようにみなし、元々のリクエストメソッドに関わらず Location ヘッダで示される値(URI)に GET リクエストをしかけている。303 や 307 は、クライアントに期待する振舞いがどちらかをサーバがはっきり示すために追加された。 <<

次、303 (See Other)。

Note: Many pre-HTTP/1.1 user agents do not understand the 303 status. When interoperability with such clients is a concern, the 302 status code may be used instead, since most user agents react to a 302 response as described here for 303.

訳) 多くの pre-HTTP/1.1 なユーザエージェントは 303 のステータスコードを理解しない。このため、このようなクライアントとの相互運用性を気にする場合は、303 の代わりに 302 を使ってもよい。だって、ほとんどのユーザエージェントは 302 に対して、ここで記述された 303 に期待される動作のように振る舞うんだもん。 <<

RFC のリダイレクトのまとめ 以上から、ぼくは POST リクエストをリダイレクトする際の期待動作を以下のように理解しました。 |Status Code|POST をリダイレクトする際の期待動作|*備考| |301|POST のままリダイレクト。ただしユーザへの通知が必要。|諦めモード| |302|POST のままリダイレクト。ただしユーザへの通知が必要。|諦めモード| |303|GET でリダイレクト|HTTP/1.1 なユーザエージェントは対応しろ| |307|POST のままリダイレクト。ただしユーザへの通知が必要。|HTTP/1.1 なユーザエージェントは対応しろ|

**各種ブラウザで実験

リダイレクトを行う PSGI App を用意して、実際のブラウザの挙動を見てみることにしました。スクリプトは最後に示しますが、パラメータとして status を要求し、そのステータスコードを返却してリダイレクトさせるというものです。

実験結果は以下のとおり。 |Status Code|期待動作|Firefox (25.0.1)|Safari(7.0)|*Chrome (31.0)| |301|POST|GET|GET|GET| |302|POST|GET|GET|GET| |303|GET|GET|GET|GET| |307|POST|POST|POST|POST|

この中で、Firefox のみが、307 に対する POST リクエストのリダイレクト時にユーザ確認ダイアログを出しました。

**総評

新しく加えられたっぽいステータスコードである 303 と 307 に対する振舞いについては、ある程度 仕様の統一が図られているようです。 一方で、301 と 302 のリダイレクトは、RFC を記述されている方々とともに諦めモードになったほうがいいのではないでしょうか。

**実験でつかったもの

|perl| use Plack::Request;

my %map = ( 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 307 => 'Temporary Redirect', );

my $app = sub { my $env = shift; my $req = Plack::Request->new($env);

my $sc = $req->param('status');

if ( defined $sc and $req->path eq '/' ) {
    return [ $sc, ['Location' => 'http://localhost:' . $req->port . "/result" ], [ $sc . $map{$sc} ]];
}
else {
    return [ 200, [], [ $req->method . ": ", $req->content ] ];
}

}; ||< 上記のスクリプトを plackup したあと、以下のような html でポチポチ押してました。

|html|

||<

***補足 2013/12/3 タイトルなおしました。