理系学生日記

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

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 を記述されている方々とともに諦めモードになったほうがいいのではないでしょうか。

**実験でつかったもの

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>
  <head></head>
  <body>
    <form method="POST" action="http://localhost:5000">
      <input type="text" name="status" size="5" />
      <input type="text" name="body"   size="10" />
      <input type="submit" value="submit" />
    </form>
  </body>
</html>

補足

2013/12/3 タイトルなおしました。 2018/08/14 はてなダイアリー記法だと崩れていたので、Markdown に置き換え