mod_gzip で Vary: * を send すると IE で SSL の挙動が変になる
まず結論から。タイトル通り mod_gzip で Vary: * が http-header として送信されると SSL 通信中の場合に限って IE(Internet Explorer) は Cache-Control とかの内容に関わらずコンテンツをキャッシュしないようです(※特に IE7 全般と IE6 特定の version が変)。本来 Vary ヘッダは proxy の挙動を決定するためのヘッダのはずなのに IE(Internet Explorer) は Vary ヘッダの内容によって挙動が変わってしまうようです。
別の言い方をすると、SSL なページでブラウザの戻るボタン押したときに「ページを表示できません」(※ie6)とか「Webページの有効期限が切れています」(※ie7)とか表示されてしまう場合は proxy を使っていて Vary: * が send されているのが原因と言えるわけです。
従ってコンテンツをキャッシュさせたい場合は Vary: * が送信されないように mod_gzip の設定を変更しなければなりません。(※SSL のコンテンツキャッシュについての善し悪しについてはここでは議論はしません。)
具体的には httpd.conf において以下のような設定をします。もっとも Vary の意味を理解していないと以下の設定は危険であることをご承知下さい。※危険性については後述。
<IfModule mod_gzip.c> .... mod_gzip_send_vary No .... </IfModule>
ちなみに実はインターネットオプションで「暗号化されたページをディスクに保存しない」にチェックが付いているだけって言うオチもあります。セキュリティ設定が高になってたりするとチェックが付いている可能性があります。
さて話を戻して Vary ヘッダと IE の挙動についてもっと深追いをしたかったのですが IE のソースコードとかないのでなかなか深追いできません。MS もソースコード公開に前向きになってきたのでそのうち深追いできるかもしれませんが、きっとそのときにはこの出来事を忘れてると思います。w
ってわけで IE が Vary ヘッダによって挙動が変わるという内容からいったんはずれて Vary ヘッダというものを知らなかったのでその調査結果まとめです。
Vary ヘッダーとはなんですか?
Apache ドキュメント 「mod_deflate - Apache HTTP サーバ」 に日本語でわかりやすい説明があります。
もし特別に何かに依存して除外したい場合、例えば User-Agent ヘッダなどに依存している場合、手動で Vary ヘッダを設定して、 追加の制限についてプロクシサーバに注意を行なう必要があります。 例えば User-Agent に依存して DEFLATE を追加する典型的な設定では、次のように追加することになります。
Header append Vary User-Agent
リクエストヘッダ以外の情報 (例えば HTTP バージョン) に依存して圧縮するかどうか決める場合、 Vary ヘッダを * に設定する必要があります。 このようにすると、仕様に準拠したプロクシはキャッシュを全く行なわなくなります。
Header set Vary *
現時点での理解力で Vary と proxy の挙動についてまとめるとこうなります。
Vary: *
proxy はコンテンツをキャッシュをしない
Vary: User-Agent
ブラウザから送信された User-Agent に応じて proxy はコンテンツキャッシュの利用を決める
Vary: Accept-Encoding
ブラウザから送信された Accept-Encoding に応じて proxy はコンテンツキャッシュの利用を決める
Vary: その他のヘッダー
ブラウザから送信された ヘッダーの内容 に応じて proxy はコンテンツキャッシュの利用を決める
Vary なし
proxy は通常動作。つまり大雑把に言うと静的コンテンツをキャッシュし動的コンテンツをキャッシュしない
さて一番下についてが問題です。Vary ヘッダが無い場合は proxy か静的コンテンツを基本キャッシュする。すると mod_gzip 側でコンテンツを圧縮したものを送信し、それが proxy にキャッシュされ、次に圧縮非対応のブラウザで同じ uri を閲覧すると圧縮されたコンテンツが表示されてしまうというわけです。つまりこのようなことが想定される使い方をしているならば Vary: * を出力しなくてはいけないというわけです。その場合は SSL + IE + 戻るボタンでキャッシュが無効でページが表示されない不具合の対処はあきらめなければなりません。
逆に proxy に別の理由でキャッシュされないことが分かっている場合は Vary ヘッダは出力しなければ万事OKなわけです。
ちなみに mod_gzip を使っているってことは apache 1.3 系を使っている訳で、その理由はおそらく mod_perl を使っているからだと思います。つまりは動的コンテンツだけを扱っている場合がほとんどではないでしょうか。そうでなければ apache 2 へ移行していて mod_deflate っていうもっと融通の利くヤツを使っっていることでしょう。というかそうした方がいろいろと良いと思います。
さてどうせなので Vary を送信しなかった場合の不具合を再現してみました。
まずは IE6 からテスト用にたてた squid 経由でサンプルコンテンツにアクセスしてみる。これが初回アクセス。squid は mod_gzip により圧縮されたコンテンツをキャッシュします。ヘッダ情報はこんな感じ。
GET http: //192.168.0.1/index.txt HTTP/1.1 Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-application, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */* Accept-Language: ja Accept-Encoding: gzip, deflate User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.04506.648; InfoPath.1) Host: 192.168.0.1 Proxy-Connection: Keep-Alive HTTP/1.0 200 OK Date: Thu, 28 Feb 2008 09:00:35 GMT Server: Apache/1.3.39 (Unix) mod_gzip/1.3.26.1a mod_perl/1.30 Last-Modified: Thu, 28 Feb 2008 08:26:12 GMT ETag: "490066-c5c-47c67024" Accept-Ranges: bytes Content-Type: text/plain Content-Encoding: gzip Content-Length: 1822 X-Cache: MISS from drk-vm-win2000 X-Cache-Lookup: MISS from drk-vm-win2000:10000 Proxy-Connection: keep-alive
つぎに mod_gzip 非対応なブラウザから同じようにアクセスしてみる。squid からは圧縮されたコンテンツが送信される。受け取り側は圧縮を展開できないので不都合発生(文字化けみたいに表示される)というわけ。
GET http: //192.168.0.1/index.txt HTTP/1.1 User-Agent: HTTP::Lite/2.1.6 Connection: close Accept: */* Host: 192.168.0.1 Server: Apache/1.3.39 (Unix) mod_gzip/1.3.26.1a mod_perl/1.30 Proxy-Connection: close Accept-Ranges: bytes X-Cache-Lookup: HIT from drk-vm-win2000:10000 Date: Thu, 28 Feb 2008 09:00:35 GMT Content-Encoding: gzip Last-Modified: Thu, 28 Feb 2008 08:26:12 GMT Content-Length: 1822 Etag: "490066-c5c-47c67024" Content-Type: text/plain X-Cache: HIT from drk-vm-win2000
ちなみに Vary: * をつけるとこんな感じのレスポンスヘッダになり squid のキャッシュが送られなくなるので正常表示になります。
Server: Apache/1.3.39 (Unix) mod_gzip/1.3.26.1a mod_perl/1.30 Proxy-Connection: close Accept-Ranges: bytes X-Cache-Lookup: MISS from drk-vm-win2000:10000 Date: Thu, 28 Feb 2008 09:33:24 GMT Vary: * Last-Modified: Thu, 28 Feb 2008 08:26:12 GMT Content-Length: 3164 Etag: "490066-c5c-47c67024" Content-Type: text/plain X-Cache: MISS from drk-vm-win2000
せっかくの機会なので Firefox 2 のソースコードで Vary 処理部分を見てみました。Vary: * は無条件に許可する処理になってます。
mozilla\netwerk\protocol\http\src\nsHttpChannel.cpp
PRBool nsHttpChannel::ResponseWouldVary() { PRBool result = PR_FALSE; nsCAutoString buf, metaKey; mCachedResponseHead->GetHeader(nsHttp::Vary, buf); if (!buf.IsEmpty()) { NS_NAMED_LITERAL_CSTRING(prefix, "request-"); // enumerate the elements of the Vary header... char *val = buf.BeginWriting(); // going to munge buf char *token = nsCRT::strtok(val, NS_HTTP_HEADER_SEPS, &val); while (token) { // // if "*", then assume response would vary. technically speaking, // "Vary: header, *" is not permitted, but we allow it anyways. // // if the response depends on the value of the "Cookie" header, then // bail since we do not store cookies in the cache. this is done // for the following reasons: // // 1- cookies can be very large in size // // 2- cookies may contain sensitive information. (for parity with // out policy of not storing Set-cookie headers in the cache // meta data, we likewise do not want to store cookie headers // here.) // // this implementation is obviously not fully standards compliant, but // it is perhaps most prudent given the above issues. // if ((*token == '*') || (PL_strcasecmp(token, "cookie") == 0)) { result = PR_TRUE; break; } else { // build cache meta data key... metaKey = prefix + nsDependentCString(token); // check the last value of the given request header to see if it has // since changed. if so, then indeed the cached response is invalid. nsXPIDLCString lastVal; mCacheEntry->GetMetaDataElement(metaKey.get(), getter_Copies(lastVal)); if (lastVal) { nsHttpAtom atom = nsHttp::ResolveAtom(token); const char *newVal = mRequestHead.PeekHeader(atom); if (newVal && (strcmp(newVal, lastVal) != 0)) { result = PR_TRUE; // yes, response would vary break; } } // next token... token = nsCRT::strtok(val, NS_HTTP_HEADER_SEPS, &val); } } } return result; }
ちなみに Vary ヘッダについて W3C では以下のように定義されています。
Vary = "Vary" ":" ( "*" | 1#field-name )
An HTTP/1.1 server SHOULD include a Vary header field with any cacheable response that is subject to server-driven negotiation. Doing so allows a cache to properly interpret future requests on that resource and informs the user agent about the presence of negotiation on that resource. A server MAY include a Vary header field with a non-cacheable response that is subject to server-driven negotiation, since this might provide the user agent with useful information about the dimensions over which the response varies at the time of the response.
A Vary field value consisting of a list of field-names signals that the representation selected for the response is based on a selection algorithm which considers ONLY the listed request-header field values in selecting the most appropriate representation. A cache MAY assume that the same selection will be made for future requests with the same values for the listed field names, for the duration of time for which the response is fresh.
The field-names given are not limited to the set of standard request-header fields defined by this specification. Field names are case-insensitive.
A Vary field value of "*" signals that unspecified parameters not limited to the request-headers (e.g., the network address of the client), play a role in the selection of the response representation. The "*" value MUST NOT be generated by a proxy server; it may only be generated by an origin server.
Vary にまるわる他の話題、より詳しい情報が必要な方は
- HTTP/1.1: Header Field Definitions
- mod_gzip のドキュメント - Caching mod_gzip compressed data using proxy servers
- Squidのvaryヘッダサポート - Snap.Shot.cx
- レスポンスヘッダ Vary - Minase's Blog - FYA
- LunaTear: IEで画像が保存出来ない時
とかもご覧になるのが宜しいかと思います。ちなみにヘッダー解析には以下のツールを用いました。どちらも大変便利でした。
- IE でヘッダ情報を覗き見るなら ieHTTPHeaders
- Firefox でヘッダ情報を覗き見るなら Live HTTP Headers :: Firefox Add-ons
ieHTTPHeaders はインストールした後にどうやって使うか一瞬悩みましたが、メニューの「表示」→「エクスプローラバー」→「ieHTTPHeaders」で ieHTTPHeaders のウィンドウが表示されるようになります。あとは勝手に Request(黒色) / Response(青色) のHTTPヘッダ情報が表示されます。
というわけで Vary 解析に飽きてきたのでココでおちまい。
コメントやシェアをお願いします!