ETag とキャッシュの関係(リベンジ→未解決)
昔の日記で試した tDiary で ETag を使う実験の続き。 まずは前回のおさらい。
- 通常、「Last-Modified」と「If-Modified-Since」が同じ場合は、キャッシュが有効になる (サーバから 304 Not Modified が返る)
- しかし、「Last-Modified」が同じでもコンテンツの内容が変わる場合がある。
- tDiary の場合は、サイドバーのプラグインが出力する内容が変わる。
- 「ETag」と「If-None-Match」を使うことで、上記の場合でもキャッシュの制御ができる(はず)。
- tDiary の場合は、なぜか「ETag」が同じでも「304 Not Modified」ではなく「200 OK」が返る。
- 200と304のどっちを返すかは、Webサーバ (Apache) が判断しているっぽい。
先に結論を
日記を書いたら長くなったので、先に結論を書いておく。 結果的にはリベンジ失敗。やっぱり原因は分からなかった。
ETag機能 | F5リロード時のステータスコード |
無効 | 304 Not Modified (キャッシュ有効) |
有効 | 200 OK (キャッシュ無効) |
tDiary で ETag が無効の場合
まずは、 ETag が無効の場合。 Web ブラウザは Firefox を使い、 Live HTTP Header でリクエストとレスポンスを取得している。
GET /diary/ HTTP/1.1 Host: www.machu.jp User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.8.0.7) Gecko/20060909 Firefox/1.5.0.7 Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5 Accept-Language: ja,en-us;q=0.7,en;q=0.3 Accept-Encoding: gzip,deflate Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7 Keep-Alive: 300 Connection: keep-alive HTTP/1.x 200 OK Date: Sun, 07 Jan 2007 11:23:40 GMT Server: Apache/1.3.37 (Unix) Cache-Control: no-cache Pragma: no-cache Vary: User-Agent Last-Modified: Fri, 29 Dec 2006 01:35:19 GMT Content-Length: 33374 Keep-Alive: timeout=3, max=8 Connection: Keep-Alive Content-Type: text/html; charset=EUC-JP
「Last-Modified」として「Fri, 29 Dec 2006 01:35:19 GMT」が返ってきている。 Webブラウザの更新ボタンを押すと、Firefoxは「If-Modified-Since」ヘッダにこの時間を付与する。
GET /diary/ HTTP/1.1 Host: www.machu.jp User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.8.0.7) Gecko/20060909 Firefox/1.5.0.7 Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5 Accept-Language: ja,en-us;q=0.7,en;q=0.3 Accept-Encoding: gzip,deflate Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7 Keep-Alive: 300 Connection: keep-alive If-Modified-Since: Fri, 29 Dec 2006 01:35:19 GMT Cache-Control: max-age=0 HTTP/1.x 304 Not Modified Date: Sun, 07 Jan 2007 11:24:15 GMT Server: Apache/1.3.37 (Unix) Connection: Keep-Alive, Keep-Alive Keep-Alive: timeout=3, max=7 Cache-Control: no-cache Vary: User-Agent
最終更新日が変わっていないので、ステータスコード200ではなく、304 Not Modifiedが返ってくる。 304の場合は日記の本文 (HTML) は送られないので、ネットワークの帯域が節約できる。
tDiary で ETag を有効にした場合
次は ETag を有効にした場合。
GET /diary/ HTTP/1.1 Host: www.machu.jp User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.8.0.7) Gecko/20060909 Firefox/1.5.0.7 Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5 Accept-Language: ja,en-us;q=0.7,en;q=0.3 Accept-Encoding: gzip,deflate Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7 Keep-Alive: 300 Connection: keep-alive Pragma: no-cache Cache-Control: no-cache HTTP/1.x 200 OK Date: Sun, 07 Jan 2007 11:30:28 GMT Server: Apache/1.3.37 (Unix) Cache-Control: no-cache Etag: "23d2c4aac21be1dde8bad3a274ec57e4" Pragma: no-cache Vary: User-Agent Last-Modified: Fri, 29 Dec 2006 01:35:19 GMT Content-Length: 33374 Keep-Alive: timeout=3, max=8 Connection: Keep-Alive Content-Type: text/html; charset=EUC-JP
「Last-Modified」だけでなく「ETag」の値も返ってきている。 Webブラウザの更新ボタンを押すと、Firefoxは「If-Modified-Since」ヘッダにこの時間を付与し、「If-None-Match」にETagの値を付与する。
GET /diary/ HTTP/1.1 Host: www.machu.jp User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.8.0.7) Gecko/20060909 Firefox/1.5.0.7 Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5 Accept-Language: ja,en-us;q=0.7,en;q=0.3 Accept-Encoding: gzip,deflate Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7 Keep-Alive: 300 Connection: keep-alive If-Modified-Since: Fri, 29 Dec 2006 01:35:19 GMT If-None-Match: "23d2c4aac21be1dde8bad3a274ec57e4" HTTP/1.x 200 OK Date: Sun, 07 Jan 2007 11:31:12 GMT Server: Apache/1.3.37 (Unix) Cache-Control: no-cache Etag: "23d2c4aac21be1dde8bad3a274ec57e4" Pragma: no-cache Vary: User-Agent Last-Modified: Fri, 29 Dec 2006 01:35:19 GMT Content-Length: 33374 Keep-Alive: timeout=3, max=8 Connection: Keep-Alive Content-Type: text/html; charset=EUC-JP
ETagの値もLast-Modifiedの値も変わっていないので、本来なら200ではなく304が返ってくるはず。 でも何故か、ETagヘッダを追加した場合は、200が返ってきている。
ちなみに、 Apache 1.3 ではなく Apache 2.0 で実験しても同じ結果だった。
静的 HTML の場合
そもそもETagの概念を勘違いしているかもと思い、単純なパターンとして普通の HTML を取得してみる。 静的な HTML の場合は、ETag の値は Web サーバ (Apache) が計算してくれる。
GET /index.html HTTP/1.1 Host: www.machu.jp User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.8.0.7) Gecko/20060909 Firefox/1.5.0.7 Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5 Accept-Language: ja,en-us;q=0.7,en;q=0.3 Accept-Encoding: gzip,deflate Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7 Keep-Alive: 300 Connection: keep-alive HTTP/1.x 200 OK Date: Sun, 07 Jan 2007 14:30:35 GMT Server: Apache/1.3.37 (Unix) Last-Modified: Sat, 28 Oct 2006 02:51:42 GMT Etag: "52dcc9-971-4542c5be" Accept-Ranges: bytes Content-Length: 2417 Keep-Alive: timeout=3, max=8 Connection: Keep-Alive Content-Type: text/html
ETagの値が返ってきたので、リロードボタンを押す。 「If-None-Match」に ETag の値が入る。
GET /index.html HTTP/1.1 Host: www.machu.jp User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.8.0.7) Gecko/20060909 Firefox/1.5.0.7 Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5 Accept-Language: ja,en-us;q=0.7,en;q=0.3 Accept-Encoding: gzip,deflate Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7 Keep-Alive: 300 Connection: keep-alive If-Modified-Since: Sat, 28 Oct 2006 02:51:42 GMT If-None-Match: "52dcc9-971-4542c5be" Cache-Control: max-age=0 HTTP/1.x 304 Not Modified Date: Sun, 07 Jan 2007 14:31:06 GMT Server: Apache/1.3.37 (Unix) Connection: Keep-Alive, Keep-Alive Keep-Alive: timeout=3, max=7 Etag: "52dcc9-971-4542c5be"
ETag の内容が変わっていないので、 200 ではなく 304 が返ってきている。 これは期待通りの動作になってる。 なぜ CGI の場合だけ上手くいかないんだろう…。
Apache のソースを見てみる
200 を返すか 304 を返すかは Web サーバ (Apache) で判断しているっぽいので、 Apache のソースを読んでみる。 1.3 より読みやすそうな Apache 2.0 のソースとしばし格闘。それっぽいコードを見つけた。 ファイルは httpd-2.0.59/modules/http/http_protocol.c の328行目付近。
※ 1.3系の場合は apache_1.3.37/src/main/http_protocol.c の579行目付近に同様のコードがある。
if_nonematch = apr_table_get(r->headers_in, "If-None-Match"); if (if_nonematch != NULL) { if (r->method_number == M_GET) { if (if_nonematch[0] == '*') { return HTTP_NOT_MODIFIED; } if (etag != NULL) { if (apr_table_get(r->headers_in, "Range")) { if (etag[0] != 'W' && ap_find_list_item(r->pool, if_nonematch, etag)) { return HTTP_NOT_MODIFIED; } } else if (ap_strstr_c(if_nonematch, etag)) { return HTTP_NOT_MODIFIED; } } } else if (if_nonematch[0] == '*' || (etag != NULL && ap_find_list_item(r->pool, if_nonematch, etag))) { return HTTP_PRECONDITION_FAILED; } } else if ((r->method_number == M_GET) && ((if_modified_since = apr_table_get(r->headers_in, "If-Modified-Since")) != NULL)) { <以下、「If-Modified-Since」と「Last-Modified」の比較のコード>
リクエストヘッダに「If-None-Match」が含まれていて、 GET メソッドの場合に if の中が実行される。 「If-None-Match」の1文字目が「*」以外で、ETagの1文字目が「W」以外の時に注目。
else if (ap_strstr_c(if_nonematch, etag)) { return HTTP_NOT_MODIFIED; }
ここで、「If-None-Match」と「ETag」の内容を比較して、一致していれば HTTP_NOT_MODIFIED (304) を返すようにしている。 このコードを見る限りは、単純に ETag の値が同じなら動きそうな気がするのに、なぜだ…。
「If-None-Match: *」で実験
もしかしたらさっきの if 文まで到達していないかも…と思った。 そこで、その少し前のコードが実行されるかどうかを実験した。
if (if_nonematch[0] == '*') { return HTTP_NOT_MODIFIED; }
リクエストヘッダに「If-None-Match: *」が含まれていたら、強制的に HTTP_NOT_MODIFIED が返るようになっている。 そこで、コマンドラインからこのリクエストを投げてみた。
$ telnet machu.jp 80 Trying 202.181.97.46... Connected to machu.jp. Escape character is '^]'. GET /diary/ HTTP/1.1 Host: www.machu.jp If-None-Match: * HTTP/1.1 304 Not Modified Date: Sun, 07 Jan 2007 14:49:01 GMT Server: Apache/1.3.37 (Unix) Cache-Control: no-cache Vary: User-Agent
今度は見事に 304 が返ってきた。 ということは、この箇所までは実行されているはず。 うーん。なんでその先が上手くいかないんだろう。
tDiary のソースを修正して、「Pragma: no-cache」や「Cache-Control: no-cache」を出力しないようにしてみても、結果は同じだった。 先は長い。