2011/5/10 以下の訂正をしました。
s/prefetch/preflight/g
JavaScriptのクロスドメイン通信で微妙な話があったので書いてみます。ちなみにクライアントサイドJavaScriptの話です。下記仕様に敬意を表して以下ではクロスオリジンと書きます。一般にクロスドメイン通信と呼ばれているものと同じ意味で使います。
WebブラウザからXMLHttpRequest(XHR)で外部のWeb APIを直接叩こうとするとクロスオリジンの制限に当たります。制限の必要性は次の説明がわかりやすいのでリンクを張っておきます。
クロスオリジン制限がある中でWebブラウザから直接Web APIを叩こうと先人は知恵を絞ってきました。iframeを使うハック的手法はマッシュマトリックスに任せておくとして、一応メインストリームな手法のひとつがJSONPです。JSONPを知らない人は自分で解説記事を見つけてください。
JSONPを使うにはWeb API提供側がJSONP対応する必要がありますが、JSON対応済みであればJSONP対応は容易です。JSONPは一応動くので現状のクロスオリジン制限を回避する手法のデファクト標準です(たぶん)。JSONPを初めて聞いた時はその巧妙さに関心しましたがこれがメインストリームの技術でいいのか、という疑念があります。そもそもJSONPにはハック臭がします。ハックはハックでメインでいいのかという思いです。たとえると、園川一美が開幕投手でいいのかに通じる思いです。scriptタグを動的生成する辺りに気持ち悪さがあります。jQueryなりのライブラリを使えばこの辺りの実装詳細が隠れるので気にしなければいいのかもしれませんが、JSONPはなにか違います(主観です)。
JSONPに代わるクロスオリジン制限回避手法のHTML5時代?のデファクトかと思うのがpostMessageです。postMessageを使うと別ドメインのWeb APIを叩くiframeを作って、元ドメインのHTMLとiframeの間で文字列の受け渡しができます。iframeをWeb APIを叩くバッファのように使ってクロスオリジン制限を回避できます。postMessage自体は綺麗なAPIだと思いますが、Web APIを叩く手法としてはやはり微妙なハック感が否めません。これもjQueryなり適当なライブラリが詳細を隠蔽すれば気にならないのかもしれませんが違和感は残ります。ロッテつながりでたとえてみると、渡辺俊介が開幕投手をするぐらいの違和感です。渡辺は良い投手だと思いますが開幕投手の格なのか、と言われると疑問です。
長い前振りが終わってようやく本題のXHR2です。
クライアント側のAPI的に美しいと思うのがXHRでそのまま別オリジンのWeb APIを叩ける手法です。冒頭に挙げたふたつのリンク、Cross-Origin Resource Sharing(CORS)とXMLHttpRequest2(XHR2)です。CORSはHTTPの世界の話で、XHR2はクライアントサイドJavaScriptのAPIの世界の話です。XHR2とありますが、クロスオリジン制限回避のためには普通に従来どおりのXHRのコードを書くだけです。XHR2に対応したWebブラウザはCORSに従ったHTTPリクエストを投げてくれます。
CORSのHTTPリクエストにはOriginリクエストヘッダがあります。リクエストURLが別オリジンのXHR呼び出しをすると、XHR2対応のWebブラウザが自動でOriginヘッダをつけてくれます。XHR2未対応の場合は別オリジンのXHR呼び出しはリクエスト自体を投げないので既に動作が異なります。Originヘッダのあるリクエストを投げてレスポンスを受けても、レスポンスが条件に合致しないとXHR2対応のWebブラウザはそのレスポンスを捨ててしまいます。条件の詳細は省略しますが、とりあえず一番簡易な場合にはAccess-Control-Allow-Originレスポンスヘッダの条件にパスする必要があります。
XHR2の簡単な動作検証のためには、リクエストを受ける側のApacheに次の設定をします。このApacheはクロスオリジンでWeb APIを叩くシナリオでWeb API提供側に当たるサーバです。
1 2 3 |
# apacheのCORSの設定例 LoadModule headers_module modules/mod_headers.so Header append Access-Control-Allow-Origin * |
クライアントサイドのJavaScriptのコードを3パターン載せます。XHR直呼び版、prototype.js版、jQuery版です。
今回の話の本質ではありませんが次のHTMLとレスポンスJSONを前提にしたコードです。
1 2 |
<div id="click" onclick="doit()">click</div> <div id="data"></div> |
1 |
{"data":"foobar"} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// XHR direct <script type="text/javascript" src="prototype17.js"></script> function doit() { var req = new XMLHttpRequest(); req.open('GET', 'http://別オリジンのホスト/ret.json', true); req.onreadystatechange = function(evt) { if (req.readyState == 4) { if (req.status == 200) { $('data').innerHTML = JSON.parse(req.responseText).data; } } }; req.send(null); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// prototype.js <script type="text/javascript" src="prototype17.js"></script> function doit() { new Ajax.Request("http://別オリジンのホスト/ret.json", { method: 'GET', onComplete: function(res) { var result = res.responseText.evalJSON(); $('data').innerHTML = result.data; } } ); } |
1 2 3 4 5 6 7 8 |
// jQuery <script type="text/javascript" charset="utf-8" src="jquery16.js"></script> function doit() { $.get('http://別オリジンのホスト/ret.json', null, function(res) { $('#data').html(res.data); }, 'json'); } |
さて、今日の本題の本題ですが、これだけではprototype.js版が期待どおりに動きません。答えはSのつくサイトにもありますが、CORSに非標準のリクエストヘッダがある場合にpreflightと呼ばれるフローがあるのが原因です。preflightはOPTIONSメソッドのリクエストを事前に投げてサーバとネゴシエーションをします。
prototype.jsのAJAX呼び出しは内部で次のような独自拡張のリクエストヘッダをつけます。これがCORSのprefetchを誘発します(prefetchはXHR2対応のWebブラウザがXHR呼び出しの内部で自動で行います)。
1 2 3 4 5 6 7 |
// prototype.js 1.7から抜粋 setRequestHeaders: function() { var headers = { 'X-Requested-With': 'XMLHttpRequest', 'X-Prototype-Version': Prototype.Version, 'Accept': 'text/javascript, text/html, application/xml, text/xml, */*' }; |
Apache側に次の追加設定をするとprototype.js版も動作するようになります。
1 2 |
# apacheのCORSの追加設定例(prototype.js) Header append Access-Control-Allow-Headers x-prototype-version,x-requested-with |
めでたしめでたし、と言いたいところですが、この結果に納得はしていません。Access-Control-Allow-Headersレスポンスヘッダは誰がつけることを想定しているのか疑問です。今回のようにApacheの設定でハードコードするのは、動作検証ではいいですが実用的には変に感じます。では、WebアプリがOPTIONSメソッドに対するレスポンスを返すべきなのでしょうか。もしそうだとしてもAccess-Control-Allow-Headersヘッダの値に何を返していいのか分かりません。
そもそも独自拡張のHTTPヘッダがあるだけで動作が変わるのは罠にしか思えません。独自拡張ヘッダは行儀が良いとは言えませんが深い考慮なしにつける人も多そうです。頭にX-をつけておけば、知らない人は無視してくれるだろうことを期待している気がします。
やはりpostMessageが本命APIでしょうか。
最近のコメント