ETag とキャッシュの関係(リベンジ→未解決)
2007-01-07
昔の日記で試した 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」を出力しないようにしてみても、結果は同じだった。 先は長い。