Hebikuzure's Tech Memo

2011年11月7日

HTTP メソッドとリダイレクト ステータス コード

Filed under: Internet Explorer — hebikuzure @ 10:37 午後

HTTP Methods and Redirect Status Codes 
http://blogs.msdn.com/b/ieinternals/archive/2011/08/19/understanding-the-impact-of-redirect-response-status-codes-on-http-methods-like-head-get-post-and-delete.aspx


更新間隔が開いてしまいましたが、今回も EricLaw’s IEInternals の記事をを私訳しました。 HTTP レスポンスでクライアントがリダイレクトされる際の挙動について、RFC の記述とブラウザーの実装の微妙な違い、またブラウザー間の挙動の相違について興味深い情報が示されています。

以下の文章は EricLaw’s IEInternals の 8/19 の記事 HTTP Methods and Redirect Status Codes を hebikuzure が私的に試訳したものです。翻訳については Microsoft Corporation および日本マイクロソフト株式会社とは無関係に hebikuzure が公開情報に基づき独自に行ったものであり、この文書の内容についての文責は公開者である hebikuzure にあります。翻訳の内容および技術的内容については正確を期すよう十分な注意を払っておりますが、誤りや不正確な部分が含まれている可能性がありますので、本文書を利用される際には原文も併せてご確認ください。


HTTP メソッドとリダイレクト ステータス コード

EricLaw [MSFT]

2011年8月19日 午前11時14分

今朝早く、このつぶやきが私の Twitter のタイムラインに現れました:

Internet Explorer が適切に動作している事をお知らせする公開サービスが必要なのかちょっと分かりませんが、そうしたからと言って害はないでしょう。とは言え情報提供が不足しているとその結果は驚くような事になります。私はこのつぶやきの元の投稿者は実際には他の事を言おうとしているのではないかと考えました。

それではちょっと解説していきましょう。HTTP/1.0 の仕様 RFC1945 では HTTP/301 は以下のように記述されています:

301 Moved Permanently
要求されたリソースは別の永続的な URL に割り当てられており、今後のすべてのこのリソースへの参照は新しいURL を使用すべきである。
// …
注意: 301 ステータスコードを受信した後の POST リクエストの自動リダイレクトの際、既存のいくつかのユーザー エージェントは誤って GET リクエストに変更する。

下線を付けた注意点は HTTP302 についての記述にも含まれています。この注意書きで言及されているこうした “ユーザー エージェント” には、当時の人気のあるブラウザー、Netscape Navigator や Internet Explorer が含まれていました。間違いなく、この挙動は正に多くの Web サイトが — 正しく処理された POST の後で何か別の物を表示するようにユーザーを異なる URL に送るために — 必要としていたものでした。とは言え POST の GET への変換の挙動は、HTTP 仕様の策定者が意図したものではありません。

RFC2616 の策定者は曖昧さをなくす事を狙って、HTTP/1.1 仕様に新しい二つのリダイレクション レスポンス コードを追加しました。

HTTP/302 の更新された定義では以下のように注記されています:

注記: RFC 1945 と RFC 2068 ではリダイレクトされたリクエストでクライアントがメソッドを変更する事を許さないと規定している。しかしながら、多くの既存のユーザー エージェントの実装では 302 を 303 レスポンスであるかのように取扱い、元のリクエスト メソッドを無視して Location フィールド値への GET を実行します。ステータス コード 303 と 307 は、クライアントがどのように反応するのを期待しているのか明確したいサーバーのために追加されています。

当時の実装者のために弁解すれば、RFC1945 と REF2068 が真にクライアントのユーザー エージェントが “メソッドを変更する事を許さない” と規定していたのであれば、彼らは明確にその通りにしたでしょう。この期待はレスポンス コードの定義において言及されていたのではなく、リダイレクトに関する助言で言及されていたのです。残念ながら現行の HTTPBIS ドラフトにも同じ制限による問題が表れていますが、この欠点は作業課題として監視されていると信じています。

RFC2616 において、新しい HTTP/303 リダイレクト コードは以下のように定義されています:

リクエストに対するレスポンスは異なる URI の下で発見でき、かつリソースへの GET メソッドで取得しなければならない。このメソッドは主に、POST により起動されたスクリプトが選択したリソースにユーザー エージェントをリダイレクトできるようにするために存在している。

つまり 303 は、リダイレクトされたリクエストのメソッドは GET に変更される事を明確に意味しています。

残念ながら、新しく導入された HTTP/307 の定義は、HTTP/302 1の元々の定義に含まれていたのと同様の問題として、最初のメソッドが利用されるべきであると明瞭に示す事に失敗しています。とは言え、(前記の文書に基づけば) HTTP/307 レスポンス コードの意図するものはクライアントがリダイレクトされたリクエストの本来のメソッドを保存すべきであるとはっきり示している事は明白です。

従って HTTP/303 と HTTP/307 の双方により、Web 開発者はメソッドに対して何が起きてほしいのか (303->GET への変換; 307->元のメソッドの維持) を示す事ができるようになったのです。

それではこれがどの位うまく成し遂げられているか、見てみましょう…

リダイレクト時のメソッドを維持する

この節では、さまざまなシナリオによるクロス ブラウザーのテストの結果を示したいと思います。この結果は XmlHttpRequest オブジェクトと Meddler Script ファイルを使って生成されています。

使用した MeddlerScript は以下の通りです:

import Meddler;
import System;
import System.Net.Sockets;
import System.Windows.Forms;
 
class Handlers
{ 
    static function OnConnection(oSession: Session)
    {
        if (oSession.ReadRequest()){        
            var oHeaders: ResponseHeaders = new ResponseHeaders();
            oHeaders.Status = "200 OK";
            oHeaders["Connection"] = "close";
            
            var sRequestAsString = oSession.requestHeaders.ToString(true, true, true) + System.Text.Encoding.UTF8.GetString(oSession.requestBodyBytes);
                        
            if (oSession.urlContains('redir'))
            {
                MessageBox.Show(sRequestAsString, "Redirect Request");
                oHeaders["Content-Type"] = "text/plain";    
                var s = oSession.GetQueryParams()["code"];
                oHeaders.Status = s + " redirect";
                oHeaders["Location"] = 'http://'+ oSession.requestHeaders["Host"] + '/final.cgi';
                oHeaders["Cache-Control"] = "max-age=0";
                oSession.WriteString(oHeaders.ToString());
                oSession.WriteString("redirecting...\r\n");
            }
            else
            if (oSession.urlContains('final'))
            {
                MessageBox.Show(sRequestAsString, "Final Request");
                oHeaders["Content-Type"] = "text/plain";    
                oHeaders["Cache-Control"] = "max-age=0";
                oSession.WriteString(oHeaders.ToString());
                oSession.WriteString("Final Request was:<br />\r\n");
                oSession.WriteString(sRequestAsString);
            }            
            else
            {
                oHeaders["Content-Type"] = "text/html";
                oSession.WriteString(oHeaders);
                oSession.WriteString("<html><head><title>XMLHTTPRequest-based Redirect Test Harness</title>\n");
                oSession.WriteString("<!doctype html>\r\n<html><head><script type='text/javascript'>\r\n");
                oSession.WriteString("var xmlHttp; var i=0;");
                oSession.WriteString("function doXHR(sMethod, sURI, sBody, iResponseCode){\r\n");
                oSession.WriteString("i++; xmlHttp = new XMLHttpRequest();\r\n");
                oSession.WriteString("\toutputdiv.innerHTML = '<pre>Beginning request #' + i + '...</pre><br />';\n");
                oSession.WriteString("\txmlHttp.onreadystatechange = stateChanged;\n");
                oSession.WriteString("\txmlHttp.open(sMethod,sURI+'?code='+iResponseCode,true);\n");
                oSession.WriteString("\txmlHttp.setRequestHeader('CustomHeader', 'CustomValue');\n");
                oSession.WriteString("\txmlHttp.send(sBody);}\n");
                oSession.WriteString("function stateChanged(){\n\t\nif (xmlHttp.readyState==4){\n data = xmlHttp.responseText; ResponseHeaders.innerHTML = '[#' + i + ']Script-accessible response headers were:<br/><PRE>' + xmlHttp.getAllResponseHeaders() + data+ '</pre>';}\n}\n");
                
                oSession.WriteString("function BuildRequest(){ doXHR(document.getElementById('txtMethod').value, document.getElementById('txtURI').value,document.getElementById('txtBody').value, document.getElementById('txtCode').value);\n}\n");
                
                
                oSession.WriteString("</script></head><body><br></p><hr>Output: <div id='outputdiv'></div><br>Response: <span id='ResponseHeaders'></div><br></span>");
                
                oSession.WriteString("<br /><input type=text id='txtMethod' value='GET' /><br />\n");
                oSession.WriteString("<input type=text id='txtURI' value='/redir.cgi' /><br />\n");
                oSession.WriteString("<input type=text id='txtBody' placeholder='Request body text...' value = '' /><br />\n");
                oSession.WriteString("<input type=text id='txtCode' value='302' />\n<br />");
                
                oSession.WriteString("<button onClick='BuildRequest();'>Send Request</button>");
                oSession.WriteString("</body></html>\n");
                
            }
            
        }
            
        oSession.CloseSocket();
    }
}

まず HTTP/303 リダイレクトを試してみました:

IE 6-10 DELETE が GET に変換される
Chrome 13 DELETE が GET に変換される
Firefox 6 DELETE が GET に変換される
Safari 5.1 DELETE が GET に変換される
Opera 11.5 DELETE が GET に変換される

すべてのブラウザーが HTTP/303 リダイレクト コードを受信した際に適切に動作しています。具体的には、クライアントは DELETE メソッドを GET に変換してリクエストを再発行しています。


次のテストとして、HTTP/307 リダイレクトを示しました:

IE 6-10 DELETE メソッドが維持される
Chrome 13 DELETE メソッドが維持される
Firefox 6 DELETE メソッドが維持される (確認後)
Safari 5.1 DELETE メソッドが維持される
Opera 11.5 リダイレクトに失敗する (確認後)

(バグがあるのではないかと思われる Opera 11.5 を除く) すべてのブラウザーが、HTTP/307 を受信した後に正しくリクエスト メソッドを維持しています。


以前からの HTTP/301 と HTTP/302 メソッドを利用した際に、開発者の皆さんもブラウザー間の相違に直面するでしょう。

IE 6-10 DELETE メソッドが維持される
Chrome 13 GET に変換される
Firefox 6 GET に変換される
Safari 5.1 GET に変換される
Opera 11.5 GET に変換される

ハイライトした結果が、Twitter での発言者が驚いた部分なのは間違いないでしょう。Internet Explorer だけが HTTP/302 リダイレクトの後でも DELETE メソッドを維持しており、RFC2616 で “誤った” としている動作を行っていません。他のブラウザーはメソッドを GET に変換しており、HTTP/301 または HTTP/302 に対して HTTP/303 と同様の応答をしています。

しかしまだびっくりする事があります…


前節で示したこの動作ですが、Internet Explorer も他のブラウザーと同様に HTTP/301HTTP/302 に対して POSTGET変換するのですから、なおさら驚くべき事でしょう:

IE 6-10 GET に変換される
Chrome 13 GET に変換される
Firefox 6 GET に変換される
Safari 5.1 GET に変換される
Opera 11.5 GET に変換される

前に示唆したように、IE が HTTP/301 と HTTP/302 に対して POST->GET 変換をするのは、1990年代に追加された、当時のより一般的だった Web ブラウザーとの互換性のための歴史的な所産です。XMLHttpRequest オブジェクトが採用される以前は、Web ページが他の HTTP メソッドを利用する事は現実的にできず、そのため POST メソッドに限って変換が行われたのです。


ブラウザー間の挙動の相違は、HEAD リクエストに対する 302 レスポンスへの対処でも顕著です:

IE 6-10 HEAD メソッドが維持される
Chrome 13 HEAD が GET に変換される
Firefox 6 HEAD が GET に変換される
Safari 5.1 HEAD が GET に変換される
Opera 11.5 リダイレクトが発生しない

HEAD が GET に予期せず変換されてしまう事により、パフォーマンス上の問題や機能上の欠陥が生じるのではないかと思われます。

リダイレクトを確認する

さて、RFC2616 ではリダイレクトは一般的に冪等/ “安全” であるメソッドを除き”自動的” に行われるべきではないと注記しています; この考え方は、同じアクションが異なる URL に対して行われる際、ユーザーがそれを確認しなければならないという事です。現実的には、ユーザーがそうしたプロンプトを理解できそうにはないので、多くのブラウザーはリダイレクトに対するユーザーの確認を求めるこの要請を無視しています。

POST -> 307 リダイレクトでの警告に関する各ブラウザーの挙動

IE 6-10 サイレントに再度 POST する
Chrome 13 サイレントに再度 POST する
Firefox 6 再 POST の前にプロンプト (ターゲット URL なし) を表示
Safari 5.1 サイレントに再度 POST する
Opera 11.5 プロンプト (ターゲット情報あり) を表示。

ただしYes でも No でも同じ結果。バグか?

Firefox のリダイレクト時の確認プロンプト:

Opera のリダイレクト時の確認プロンプト (: 三つのボタンのどれをクリックすれば良いのかという問題には見えません)

この記事が、状況を明確にするのに役立てばと願っています。一般的に言えば、もし GET 以外のメソッドを使っている際にリダイレクトをさせようと思ったら、どのような動作が望ましいのか明らかにするため HTTP303/ と HTTP/307 を使いましょう。

-Eric

1件のコメント »

  1. […] 以前に紹介した IEInternals の記事「HTTP Methods and Redirect Status Codes」(私訳) で検討されていた HTTP リダイレクト ストータス […]

    ピンバック by HTTP/308 で Web を前進させよう « Hebikuzure's Tech Memo — 2012年3月30日 @ 7:38 午後


RSS feed for comments on this post. TrackBack URI

コメントを残す

WordPress.com Blog.