«前の日記(2006-08-18 (金)) 最新 次の日記(2006-08-28 (月))»  

まちゅダイアリー


2006-08-26 (土)

キャッシュと Last-Modified の関係

tDiary に認証機能を組み込む実験をしてから二ヶ月経つ。 日記のナビゲーションにログイン/ログアウトボタンを付けたんだけど、一つ問題があった。 それは、ログアウトした後でもナビゲーションに「ログアウト」ボタンが表示されたままになるということ。 本当はログアウト後は「ログイン」ボタンが表示されるはずなのに。

少し時間ができたので、 HTTP ヘッダの内容をキャプチャして調べてみた。 結論としては、ログインの前後で同じ Last-Modified を返しているので、ブラウザのキャッシュが使われているのが原因だった。

「tDiary のトップページを表示」→「ログインボタンを押してログイン画面を表示」→「ログイン画面にIDとパスワードを入れてログイン」までを実行したところからキャプチャをスタート。

トップページの表示(ログイン後)

ログインした後にトップページを表示したときのヘッダ。

GET /sample/tdiary-auth/ HTTP/1.1
Host: www.machu.jp
(略)
Cookie: _session_id=24c72f72c8abd6e403df911fb3099476
HTTP/1.x 200 OK
Date: Sat, 26 Aug 2006 16:22:15 GMT
Server: Apache/1.3.37 (Unix)
(略)
Set-Cookie: _session_id=24c72f72c8abd6e403df911fb3099476; path=/sample/tdiary-auth
Last-Modified: Wed, 21 Jun 2006 07:00:25 GMT

帰ってきた画面には、ナビゲーションに「ログアウト」ボタンが含まれている。 レスポンスの Last-Modified に注目。 これは、コンテンツ(日記)が最後に更新された日時になる。 Web ブラウザはこの情報を使って、キャッシュを使うかどうかを判断する。

ログアウト

ログアウトボタンをクリックしたときのヘッダ。 まぁ、ここは本題じゃないのでどうでもいい。

GET /sample/tdiary-auth/?logout=true HTTP/1.1
Host: www.machu.jp
(略)
Cookie: _session_id=24c72f72c8abd6e403df911fb3099476
HTTP/1.x 200 OK
Date: Sat, 26 Aug 2006 16:22:22 GMT
Server: Apache/1.3.37 (Unix)
(略)
Set-Cookie: _session_id=24c72f72c8abd6e403df911fb3099476; path=/sample/tdiary-auth

トップページへ戻るためのリダイレクト画面(Wait or Click here! の画面)なので、レスポンスに Last-Modified ヘッダは含まれない。 ちなみに、ログアウトだから本来はセッション Cookie を削除すべきだけど、僕が実装をサボっている。

トップページの表示(ログアウト後)

リダイレクト画面を受信すると、自動的にトップページを取得しに行く。

GET /sample/tdiary-auth/ HTTP/1.1
Host: www.machu.jp
(略)
Cookie: _session_id=24c72f72c8abd6e403df911fb3099476
If-Modified-Since: Wed, 21 Jun 2006 07:00:25 GMT
HTTP/1.x 304 Not Modified
Date: Sat, 26 Aug 2006 16:22:23 GMT
Server: Apache/1.3.37 (Unix)

Web ブラウザからのリクエストに「If-Modified-Since」ヘッダが送られている。 これは、ログイン後にトップページを表示したときに、レスポンスに含まれていた Last-Modified と同じもの。 「07:00:25 以降に更新されていたら新しいのをください」と要求している。

これに対して、サーバからは「304 Not Modified」として、「ページは更新されてないよ」というレスポンスが返ってきている。 確かに、ログイン前後で変わるのはナビゲーションのボタンだけで、日記が更新されている訳じゃない。 だから、この動作自体は間違っていない。 とはいえ、ボタンの内容は変わってくれないと困る。 Last-Modified ヘッダを返さないようにすれば問題は解決するけど、それじゃ根本的な解決にはならない。

ETag を使ってキャッシュ制御(未解決)

ってことで、ページの内容(ログイン/ログアウトのナビゲーション)が違うけど、日記の内容は同じ (Last-Modified が同じ) 場合にどうすればいいか。 Web を調べていたら「ETag」というものが使えそうだと気がついた。 ETag について Web ではあまり情報が見つからなかった(@ITHTTP入門Studying HTTPくらい?)。 要はそのページを一意に特定するための ID みたいなもので、キャッシュ制御に利用するらしい。

キャッシュなら「Last-Modified」と「If-Modified-Since」の組み合わせだけでいいじゃん、と思っちゃうけど、そうでもないみたい。 例えば、1つの URL に対して複数のリソース(英語のページと日本語のページなど)が存在する場合がそう。 もう少し具体的にいうと、 http://example.com/index.html という URL があったとして、日本語版の Web ブラウザでアクセスすると日本語のページが、英語版の Web ブラウザでアクセスすると英語のページが返ってくるようになっていた場合*1、最終更新日は同じでも言語によってページの内容が違うことになる。 んで、そういった場合でも、それぞれに違う ETag を付けておけば、 Web ブラウザ側でどっちのキャッシュを持っているかを明示できるって訳。

同じように、ログイン前後のページに違う ETag を付けておけば、日記の更新日時が同じでもキャッシュ制御が上手くいくんじゃないかと期待。

試してみる(事前準備)

あれこれ考えるよりも、まずは試してみる。 tDiary の index.rb を見てみると、ラッキーなことに既に「ETag testing code」が書かれていた。 ソースはコメントアウトされていたので、有効にしてみる。

@@ -66,7 +66,7 @@
                head['Last-Modified'] = CGI::rfc1123_date( tdiary.last_modified )

                # ETag testing code
-               # require 'md5'
+               require 'md5'
                # head['ETag'] = MD5::md5( body )

                if /HEAD/i !~ @cgi.request_method then
@@ -82,6 +82,7 @@
                                head['Cache-Control'] = 'no-cache'
                        end
                        head['cookie'] = tdiary.cookies if tdiary.cookies.size > 0
+                       head['ETag'] = %Q("#{MD5::md5( body )}")
                        print @cgi.header( head )
                        print body

日記本文のハッシュ値 (MD5) が ETag の値になっている。 元のソースでは body の生成前に ETag を作っていたので、これを body 生成後に移動。 また、HTTP の仕様書では、 ETag は quoted-string と定義されているので、前後を「"」で囲むようにした。 準備ができたので、実際にデータを取ってみる。

トップページの表示(ログイン後)

まずはログイン後のトップページから。

GET /sample/tdiary-auth/ HTTP/1.1
Host: www.machu.jp
(略)
Cookie: _session_id=9b0f837d027031711733eae4524d7479
HTTP/1.x 200 OK
Date: Sun, 27 Aug 2006 03:37:38 GMT
Server: Apache/1.3.37 (Unix)
Cache-Control: no-cache
Etag: "a436b57f1f146309a6fd3989c37ae0d0"
Pragma: no-cache
Vary: User-Agent
Set-Cookie: _session_id=9b0f837d027031711733eae4524d7479; path=/sample/tdiary-auth
Last-Modified: Wed, 21 Jun 2006 07:00:25 GMT
(略)

Last-Modified だけでなく、 Etag が返されるようになっている。

ログアウト

ログアウトボタンをクリックした場合のヘッダ。 先ほどと同じく、ここは重要じゃない。

GET /sample/tdiary-auth/?logout=true HTTP/1.1
Host: www.machu.jp
(略)
Cookie: _session_id=9b0f837d027031711733eae4524d7479
HTTP/1.x 200 OK
Date: Sun, 27 Aug 2006 03:38:05 GMT
(略)

この後、自動的にトップページへ戻る。

トップページの表示(ログアウト後)

GET /sample/tdiary-auth/ HTTP/1.1
Host: www.machu.jp
(略)
If-Modified-Since: Wed, 21 Jun 2006 07:00:25 GMT
If-None-Match: "a436b57f1f146309a6fd3989c37ae0d0"

先ほどの ETag を使わない例では「If-Modified-Since」だけだったんだけど、今度は「If-None-Match」が追加されている。 この値は、ログイン後にトップページを表示したときに、サーバから返ってきた「ETag」の値になっている。 『「07:00:25」に更新された「a436b57...」という ETag のキャッシュをもってますよ』とサーバに伝えている。

HTTP/1.x 200 OK
Date: Sun, 27 Aug 2006 03:38:06 GMT
Server: Apache/1.3.37 (Unix)
Cache-Control: no-cache
Etag: "e72a783e48a9d08453d7690b04397a64"
Pragma: no-cache
Vary: User-Agent
Last-Modified: Wed, 21 Jun 2006 07:00:25 GMT
(略)

サーバからのレスポンスも「304 Not Modified」から、「200 OK」へと変わっている。 この時も ETag を返しているけど、ログイン後の値「a436b57...」じゃなくて「e72a783...」になっている。 これは、ナビゲーションのリンクが「ログアウト」から「ログイン」に変わったため。

ってことで、 ETag を使わない場合はログイン中のページがログイン後も表示されていたけど、 ETag を使うことでキャッシュが使われなくなった。

課題(F5リロードで304が返らない)

これでめでたしと思ったら、そんなに甘くなかった。 試しに F5 でリロードしてみる。

GET /sample/tdiary-auth/ HTTP/1.1
Host: www.machu.jp
(略)
If-Modified-Since: Wed, 21 Jun 2006 07:00:25 GMT
If-None-Match: "e72a783e48a9d08453d7690b04397a64"

最終更新日も ETag も変わらないから、本来なら 304 が返るはずだけど…。

HTTP/1.x 200 OK
Date: Sun, 27 Aug 2006 04:33:30 GMT
Server: Apache/1.3.37 (Unix)
Cache-Control: no-cache
Etag: "e72a783e48a9d08453d7690b04397a64"
Pragma: no-cache
Vary: User-Agent
Last-Modified: Wed, 21 Jun 2006 07:00:25 GMT
(略)

なぜか 200 が返ってきてしまう orz 。 これじゃ、キャッシュを無効にした場合と変わらないよ。

ステータスコード 200 と 304 のどっちを返すかは、 tDiary ではなく Web サーバ (Apache) が判断しているっぽい。 うーん。これ以上調べるか、それともここで割り切るか…。

*1 実際には英語版、日本語版は関係なく、 Web ブラウザが欲しい言語をリクエストヘッダに載せて制御している