HTTPの304応答、本文を送らない再検証で何が減るのか

白い器に淡い緑の折り畳まれた面が収まり、その上に小さな桃色の球と薄紫の弧が浮かぶ抽象的な構図。

同じ画像を何度も表示するとき、毎回すべてのデータを送り直す必要はあるのでしょうか。手元にある画像が今も使えるなら、本文をもう一度送らずに済みます。ただし、「通信しなかった」と「通信して変わっていないことを確かめた」は違います。

HTTPの304 Not Modifiedは、条件付きの読み取りで、保存済みの表現を利用できることを知らせる応答です。[1] 画像を20回確認する小さな例から、何が減り、何が残るのかを考えます。

簡単にまとめると?

  • 保存済みのETagをIf-None-Matchで送り、選ばれた表現と一致すれば、GETへの応答は本文なしの304になり得ます。
  • 例では20回の確認中に画像の版が4回登場すると、画像本文の転送は20回から4回へ減ります。確認のための要求と応答は残ります。
  • 304を使うには、対応する保存済み本文が必要です。新鮮なキャッシュを通信なしで使う場合や、更新競合を防ぐIf-Matchとは役割が違います。

手元の版を伝えてから、同じ画像を尋ねる

最初のGETで画像を受け取り、応答にETag: "image-a"が付いていたとします。クライアントは画像本文と、それに対応するETagを保存します。次に再検証が必要になったとき、要求へIf-None-Match: "image-a"を付けます。

名前の通り、これは「指定したタグのどれとも一致しないなら」という条件です。サーバーが今回の要求に対して選んだ表現のETagと一致する場合、条件は偽になります。GETなら304を返し、画像本文を送り直しません。一致しなければ、この条件は画像の転送を妨げず、通常の取得処理へ進みます。[1] 認証失敗や対象不存在など、別の理由による応答まで200へ変える仕組みではありません。

304は、画像が空になったという意味ではありません。新しい画像本文を運ぶ応答ではなく、対応する保存済みの画像を使うための応答です。本文を失ってETagだけが残っていると、304だけから画像を復元することはできません。保存データと検証用の情報を対応させて扱う必要があります。

20回確認したとき、減るのはどの部分か

説明用に、画像の大きさを各版とも1 MiB、1,048,576バイトとします。20回の読み取りのうち、1〜5回目は版A、6〜10回目は版B、11〜15回目は版C、16〜20回目は版Dです。最初は手元に画像がなく、毎回サーバーへ確認しに行く条件を置きます。再検証なしで使える新鮮なキャッシュの場面は、ここでは混ぜません。

毎回無条件で画像を取得する比較方式では、画像本文は20 MiBです。ETagを保存し、2回目以降を条件付きGETにする方式では、1・6・11・16回目に新しい版の本文を受け取り、そのほか16回は304となります。画像本文だけなら4 MiBで、差は16 MiB、80%です。これは設定した版の変化から計算した値であり、実サイトで測った削減率ではありません。

独自モデルの20回取得。毎回無条件の方式は画像本文20MiB、条件付きGETは4MiB。どちらも要求20回で、条件付き方式では200が4回、304が16回。
独自モデルの画像本文量。版の変化は6・11・16回目、各版は1 MiB。ヘッダー、暗号化・通信の付加情報、要求本文などは計算に含めていません。

要求の回数は両方とも20回です。304でも要求と応答の往復はあるので、本文が80%減ったことから、待ち時間も80%短くなったとは言えません。小さな画像では往復時間やヘッダーの割合が大きくなるかもしれませんし、大きな画像では本文の転送を省く効果が目立つかもしれません。実測では、本文量、全転送量、要求数、応答時間を分けると理由を追いやすくなります。

通信しないキャッシュ利用とは分けて考える

HTTPキャッシュには、保存した応答を新鮮と判断して、毎回の再検証なしに使える場面があります。新鮮さは有効期間と現在の年齢などから判断されます。[2] この場合は、今回の20回確認モデルとは要求数から違います。

例えば同じ画像を5回表示しても、最初に取得した応答をそのまま再利用できる設定と状況なら、表示のたびにネットワーク要求を出す設計にはなりません。一方、再検証を行って304を受け取る方式では、本文の送り直しを避けても確認の通信は残ります。「キャッシュが効いた」を一つの数字だけで扱うと、この二つを混同しやすくなります。

運用上は、画像の内容が変わったときにどれだけ早く反映させたいか、再検証なしで使える期間をどう設けるか、保存容量や削除時にどう復旧するかを合わせて考えます。304を多くすること自体を目標にするより、必要な新しさを守りながら、不要な本文転送や要求を減らすという順序の方が判断しやすくなります。

ETagが同じでも、何の一致かを確認する

If-None-Matchは弱い比較を使います。W/"image-a"という弱いETagも再検証に利用でき、強いETagと弱いETagの接頭辞だけを違いとして不一致にはしません。[1] したがって、304を受け取ったことを、手元とサーバーの全バイトが同一であるという一般的な証明へ広げてはいけません。

また、同じURLでも、要求によって異なる表現が選ばれることがあります。HTTPキャッシュは、Varyなどに基づく応答の選択条件も扱います。[2] 例えば表示言語ごとに本文が違う資源で、URLだけを見て別の言語の本文を再利用する独自実装は、ETagの比較以前に保存対象の選び方を誤っています。どの要求に対応する本文なのか、という対応付けを省けません。

編集の競合を防ぐIf-Matchでは、強い比較で一致することを更新の条件にします。ここで扱うIf-None-Matchの再検証とは、条件の向きも目的も違います。似た名前を置き換えるのではなく、「古い編集による上書きを防ぐ」のか、「既に持っている表現の送り直しを避ける」のかを先に決めます。

304の多さを成果へ読み替えない

今回のモデルでは、版が全く変わらなければ本文は最初の1回だけ、毎回変われば20回とも必要になります。同じ条件付きGETでも、更新頻度によって結果が変わります。実装を確かめるときは、変化なし、途中で変化、保存済み本文なし、異なる表現の要求、というケースを分けると確認しやすくなります。

304の件数だけでは、節約した量も、利用者が待った時間も分かりません。保存済み本文を正しく選べたことを前提に、何回の往復を行い、どれだけの本文を受け取り、必要な更新がいつ反映されたかまで追うと、再検証の役割を具体的に評価できます。

参考資料

  • [1] R. Fielding, M. Nottingham, J. Reschke (eds.), RFC 9110: HTTP Semantics, June 2022, STD 97。8.8.3.2、13.1.2、15.4.5節を参照。
  • [2] R. Fielding, M. Nottingham, J. Reschke (eds.), RFC 9111: HTTP Caching, June 2022, STD 98。4.1節のVary、4.2節の新鮮さ、4.3.4節の304による保存済み応答の更新を参照。20回取得の版構成・計算・図は独自に作成しました。