HTTPキャッシュの「絶対時刻」と「相対時間」:ExpiresとCache-Controlの仁義なき戦い
ネットワークエンジニアとして現場に立っていると、「ブラウザがキャッシュを破棄してくれない」「サーバー側の設定を変更したはずなのに、古いリソースが配信され続ける」という相談をよく受ける。
その原因の9割は、HTTPキャッシュ制御の優先順位の理解不足だ。特に、HTTP/1.0時代の遺産である`Expires`と、HTTP/1.1で導入された`Cache-Control`が混在する環境では、挙動に「罠」が潜んでいる。
今日は、この新旧規格がせめぎ合うキャッシュ制御の深淵を紐解いていこう。
—
1. なぜ「Expires」と「Cache-Control」が混在するのか
歴史を振り返れば、`Expires`はHTTP/1.0(1996年)に登場した。これは「このリソースは〇〇日の〇〇時まで有効」という絶対時刻を指定するものだ。非常にシンプルだが、サーバーとクライアントの時計が同期されていないと死ぬ。
一方、HTTP/1.1(1999年)で登場した`Cache-Control`は、`max-age`という概念を用いて、「レスポンスを受け取ってから〇〇秒間有効」という相対時間を指定する。
現場で両方が混在している理由は、単なる「後方互換性の確保」だ。古いクライアントやプロキシを救うために`Expires`を書きつつ、モダンなブラウザのために`Cache-Control`を併記する。この「優しさ」が、時にエンジニアを苦しめることになる。
2. 優先順位の決定版:RFCのルール
結論から言おう。RFC 7234(および最新のRFC 9111)の規定では、`Cache-Control: max-age`が存在する場合、`Expires`は無視される。
キャッシュの決定権は、常に新しい規格である`Cache-Control`が握っている。この「上書き」のルールさえ覚えておけば、デバッグ時に迷うことはない。
キャッシュ判定のフロー(簡略化)
1. `Cache-Control`ヘッダーがあるか?
- `max-age` があれば、それを優先。終了。
2. なければ `Expires` ヘッダーがあるか?
- あれば、その絶対時刻を有効期限として採用。
3. どちらもなければ?
- ヒューリスティックキャッシュ(ブラウザの推測)に委ねるか、原則として「毎回再検証」となる。
—
3. 実践:挙動を検証するためのコード
百聞は一見にしかず。curlを使って、サーバーが何を返しているか確認し、その挙動をシミュレートしてみよう。
curlでヘッダーを確認する
サーバーが返すヘッダーを詳細にチェックする
curl -I https://example.com/api/resource
もし、サーバー設定が以下のように混在していたらどうなるか。
HTTP/1.1 200 OK
Date: Mon, 20 May 2024 10:00:00 GMT
Cache-Control: max-age=3600 # 相対時間: 1時間
Expires: Mon, 20 May 2024 10:00:00 GMT # 絶対時刻: すでに期限切れ
この場合、`Cache-Control`が優先されるため、クライアント(ブラウザ)は11:00までこのリソースをキャッシュする。`Expires`の期限が切れているからといって、キャッシュが効かないわけではないのだ。
Python (Requests) でのキャッシュ制御
Web APIを叩く側でも、キャッシュの挙動を意識する必要がある。
import requests
キャッシュをバイパスして最新を取得したい場合
headers = {
‘Cache-Control’: ‘no-cache’, # 強制的に再検証を要求
‘Pragma’: ‘no-cache’ # HTTP/1.0クライアント用
}
response = requests.get(‘https://example.com/api/data’, headers=headers)
レスポンスのキャッシュ制御ヘッダーを確認するデバッグ用ログ
print(f”Cache-Control: {response.headers.get(‘Cache-Control’)}”)
print(f”Expires: {response.headers.get(‘Expires’)}”)
—
4. インフラ屋としての現場Tips
実務で私が最も推奨するのは、「`Expires`を捨て、`Cache-Control`一本に絞る」ことだ。
現在のWeb環境において、HTTP/1.0のみをサポートする環境を考慮する必要はほぼない。`Expires`を併記することは、運用上のミス(時刻のズレによるキャッシュの不整合など)を誘発するリスクの方が遥かに高い。
Nginxでの設定例
もしあなたがNginxでキャッシュを制御しているなら、以下のようにシンプルに記述することをお勧めする。
1時間キャッシュさせる設定
location /static/ {
# Expiresをわざわざ書く必要はない
add_header Cache-Control “max-age=3600, public”;
}
最後に:トラブルシューティングの極意
もし、「キャッシュが効かない!」というクレームが来たら、まず以下の順序で確認してほしい。
1. `Date`ヘッダーの確認: サーバー側の現在時刻がずれていないか?(絶対時刻指定の場合、これが命取りになる)
2. `Vary`ヘッダーの確認: `Vary: Accept-Encoding`などがあると、キャッシュの条件が複雑になる。
3. ブラウザのDevTools: Networkタブを開き、”Size”項目が `(disk cache)` になっているか、あるいは `304 Not Modified` が返されているかを確認する。
プロトコルの仕様を正しく理解することは、単なる知識の蓄積ではない。それは、複雑なネットワークという迷宮で、唯一頼れる「コンパス」を持つことと同義だ。
皆さんのインフラ運用が、今日も安定して稼働することを願っている。何か詰まったら、またパケットの向こう側で会おう。
コメント