HTTPのキャッシュ無効化、更新したページと一覧が同時に変わるとは限らない

緑と薄紫の二つの台に、三枚ずつの白い板が立つ。緑の台の一枚だけが横向きで、桃色のリボンがその板へ触れている。

商品の名前を変更し、保存は成功したのに、一覧画面には古い名前が残る。HTTPのキャッシュがあるシステムでは、更新の成功と、関連するすべての表示が新しくなることを分けて考える必要があります。詳細ページと一覧ページでは、保存されている応答のURLも、応答を持っているキャッシュも違うかもしれません。

RFC 9111のキャッシュ無効化の規則は、更新に伴って何を再利用できなくするかを定めています。ただし、アプリケーションのデータ間の関係をすべて理解して、世界中のキャッシュへ変更を伝える仕組みではありません。対象のURL、関連URL、要求が通った経路の三つを分けて、その範囲を見ます。

簡単にまとめると?

  • 状態を変え得る要求に対して2xxまたは3xxの応答を受けたキャッシュは、要求対象のURIを無効化します。
  • 無効化は、保存内容を取り除くか、次に送る前の検証を必須にすることです。更新済みの本文をその場で全キャッシュへ配ることではありません。
  • 関連する一覧や、別経路のキャッシュまで自動的に無効化される保証はありません。更新対象と表示の依存関係を、アプリケーション側でも設計します。

更新の成功から分かるのは、どの範囲か

HTTPでは、PUT、POST、DELETEのように、元のサーバーの状態を変え得るメソッドをunsafeと呼びます。ここでの「安全でない」は、通信が暗号化されていない、という意味ではありません。要求に状態変更の可能性があるかどうかの区別です。

キャッシュがこうした要求に対してエラーではない応答を受け取ると、要求対象のURIを無効化しなければなりません。安全性が分からないメソッドも、この規則に含まれます。RFC 9111でいうエラーではない応答は2xxまたは3xxです。200だけに限定した規則ではありません。[1]

ただし、応答コードを受けたことから、業務上の処理がすべて完了したと推定するわけではありません。ここで定めているのは、キャッシュが保存済み応答を無効化する契機です。アプリケーションの処理完了や、その後の読み取り結果の確認とは分けて扱います。

同じデータを表示していても、URIは別のことがある

例として、詳細の/items/7、一覧の/items、検索結果の/search?q=lampという三つのURIがあるとします。どれも商品7の名前を含みますが、HTTPから見れば別の応答です。

キャッシュAを通してPUT /items/7を送り、2xxの応答が戻ったとします。この経路にあるAは、/items/7について保存していた応答を無効化します。同じ対象URIに複数の保存済み応答がある場合も、無効化の対象になります。[1]

一方、この規則だけから、/itemsや検索結果まで必ず無効化されるとは言えません。HTTPのキャッシュが、一覧の中に商品7の名前が含まれているという依存関係を、当然に知っているわけではないからです。詳細を読み直したら新しい名前になり、一覧では古い応答が再利用される、という組合せが残り得ます。

さらに別の利用者がキャッシュBを通っていた場合、更新要求がBを通らなければ、Bには今回の無効化の契機が届きません。RFC 9111も、状態変更の要求が無効化するのは、その要求が通るキャッシュ内の応答であり、世界全体で適切な応答がすべて無効化される保証ではない、と注意しています。

更新要求が通ったキャッシュAでは詳細URIだけが無効化対象。一覧と検索はこの規則だけでは保証されず、要求が通らないキャッシュBの三つの応答にも今回の契機は届かない。
要求対象のURIだけを無効化する独自モデル。追加の無効化、検証、期限切れが起きない条件で、規則が保証する範囲を示しています。

無効化は、新しい内容への置き換えとは違う

無効化には、対象URIの保存済み応答を取り除く方法と、「次に送る前には必ず検証する必要がある」と印を付ける方法があります。必ずディスクから物理的に削除するという意味ではありません。

また、PUTへの応答が成功しても、それが次のGETで返す完全な本文と同じとは限りません。キャッシュ無効化は、古い応答をそのまま再利用してしまうことに歯止めをかける規則です。関連する表示へ新しい本文を一斉に複製する処理とは別です。

設計や試験では、単にキャッシュのファイルが残っているかどうかを見るだけでは不十分です。次の要求に対して、保存済みの応答を検証せず返したのか、検証したうえで使ったのか、取得し直したのかを区別します。

Locationがあっても、関連先すべての更新を約束しない

規格は、エラーではない応答を受けたキャッシュが、要求対象以外のURIも無効化することを認めています。応答のLocationやContent-LocationにあるURIは、その候補です。ただし、対象URIの無効化が必須であるのに対し、こうした追加の無効化は任意です。[1]

そのため、応答にLocationを付けたという事実だけで、そこをすべてのキャッシュが必ず無効化すると扱うことはできません。実際に使うキャッシュの挙動や、別途定めた連携方式を確認する必要があります。

この規則による追加の無効化では、要求対象と異なるオリジンのURIを無効化してはいけません。別サイトの保存内容を無関係な応答から消させるような、サービス妨害を防ぐための制約です。単にリンク先だから、同じ画面に載るから、という理由で無効化の範囲が広がるわけではありません。

読者に見せたい整合性から、試験範囲を決める

更新後の表示を確かめるなら、少なくとも三つの問いを分けます。更新対象そのものは次の読み取りでどうなるか。同じデータを含む一覧や検索結果はどうなるか。別の利用者や経路のキャッシュには、いつ変更が伝わるか、です。

一覧の古さを一定時間許容する設計も、保存直後には本人だけ必ず最新の表示を読む設計もあります。どちらが必要かは用途によります。HTTPの対象URIに関する規則だけで、その要件が満たされたとは言えません。関連URIを明示して無効化する仕組みや、適切な再検証方針などを選ぶ場合も、その範囲と失敗時の挙動を確認します。

今回の模型では、Aの詳細だけが無効化対象になり、ほかの五つにはこの契機による無効化を適用していません。これは、あらゆる製品が必ず五つを古いまま返すという予測ではありません。追加の機能を使わず、要求経路と対象URIだけを見たときに、何を保証できるかを分けるための例です。保存成功の表示から全画面の更新を推定せず、読者が実際にたどる表示と経路に沿って確かめることが大切です。

出典

  • [1] RFC 9111: HTTP Caching(2022年6月) — 第4.4節「Invalidating Stored Responses」。要求対象の必須無効化、ほかのURIの任意無効化、異なるオリジンの制約、2xx・3xx、経路を通らないキャッシュの限界を参照。三つのURIと二つのキャッシュの図は独自モデルです。