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 されているのが原因と言えるわけです。

img01.jpgimg04.jpg

従ってコンテンツをキャッシュさせたい場合は Vary: * が送信されないように mod_gzip の設定を変更しなければなりません。(※SSL のコンテンツキャッシュについての善し悪しについてはここでは議論はしません。)
具体的には httpd.conf において以下のような設定をします。もっとも Vary の意味を理解していないと以下の設定は危険であることをご承知下さい。※危険性については後述。

<IfModule mod_gzip.c>
    ....
    mod_gzip_send_vary    No
    ....
</IfModule>
- スポンサーリンク -

ちなみに実はインターネットオプションで「暗号化されたページをディスクに保存しない」にチェックが付いているだけって言うオチもあります。セキュリティ設定が高になってたりするとチェックが付いている可能性があります。

img02.jpg


さて話を戻して Vary ヘッダと IE の挙動についてもっと深追いをしたかったのですが IE のソースコードとかないのでなかなか深追いできません。MS もソースコード公開に前向きになってきたのでそのうち深追いできるかもしれませんが、きっとそのときにはこの出来事を忘れてると思います。w

ってわけで IE が Vary ヘッダによって挙動が変わるという内容からいったんはずれて Vary ヘッダというものを知らなかったのでその調査結果まとめです。


Vary ヘッダーとはなんですか?

Apache ドキュメント 「mod_deflate - Apache HTTP サーバ」 に日本語でわかりやすい説明があります。

mod_deflate モジュールは Vary: Accept-Encoding HTTP 応答ヘッダを送信して、適切な Accept-Encoding リクエストヘッダを送信するクライアントに対してのみ、 プロクシサーバがキャッシュした応答を送信するように注意を喚起します。 このようにして、圧縮を扱うことのできないクライアントに 圧縮された内容が送られることのないようにします。
もし特別に何かに依存して除外したい場合、例えば 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 では以下のように定義されています。

The Vary field value indicates the set of request-header fields that fully determines, while the response is fresh, whether a cache is permitted to use the response to reply to a subsequent request without revalidation. For uncacheable or stale responses, the Vary field value advises the user agent about the criteria that were used to select the representation. A Vary field value of "*" implies that a cache cannot determine from the request headers of a subsequent request whether this response is the appropriate representation. See section 13.6 for use of the Vary header field by caches.
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 にまるわる他の話題、より詳しい情報が必要な方は

とかもご覧になるのが宜しいかと思います。ちなみにヘッダー解析には以下のツールを用いました。どちらも大変便利でした。

ieHTTPHeaders はインストールした後にどうやって使うか一瞬悩みましたが、メニューの「表示」→「エクスプローラバー」→「ieHTTPHeaders」で ieHTTPHeaders のウィンドウが表示されるようになります。あとは勝手に Request(黒色) / Response(青色) のHTTPヘッダ情報が表示されます。


というわけで Vary 解析に飽きてきたのでココでおちまい。

- スポンサーリンク -