HTTP ETagと条件付きリクエスト: If-None-Matchの304、If-Matchの412

ETagはキャッシュの再検証と同時更新の防止の両方に使われます。RFC 9110とRFC 9111をもとに、強い比較と弱い比較、If-None-Matchと304、If-Matchと412、条件ヘッダーの評価順序をリクエスト例とともに整理します。

httpcacheetagconditional-requests

レスポンスのETagの値には二つの役割があります。一つは、キャッシュが保存しているレスポンスをまだ使えるかどうかをオリジンサーバーに確かめること。もう一つは、二つのクライアントが同じリソースを同時に書き換えたとき、片方の変更が黙って上書きされるのを防ぐことです。前者はIf-None-Matchと304で、後者はIf-Matchと412で行います。ヘッダー名は似ていますが、比較の方法も、失敗したときのレスポンスも違います。

この記事は、HTTPのセマンティクスを定めるRFC 9110(2022年6月)と、HTTPキャッシュを定めるRFC 9111(2022年6月)の該当節をもとにしています。特定のCDN、ブラウザー、Webフレームワークの挙動は扱いません。/notes/42のようなリソースやヘッダーの値は、説明のために作った例です。

ETagはクライアントが解釈しない値

RFC 9110の8.8.3節は、エンティティタグ(entity tag)を、同じリソースの複数の表現を区別するための不透明な(opaque)検証子と定義しています。表現が複数ある理由が時間とともに状態が変わったことでも、コンテンツネゴシエーションで複数の表現が同時に有効なことでも、その両方でもかまいません。値はダブルクォートで囲んだ文字列で、先頭に弱さを示すW/が付くことがあります。

ETag: "xyzzy"
ETag: W/"xyzzy"
ETag: ""

値の作り方はサーバーが決めます。8.8.3.1節は、内部のリビジョン番号(コンテンツネゴシエーションがあれば変異の識別子を組み合わせる)、表現内容の衝突耐性ハッシュ、複数のファイル属性の組み合わせ、1秒より細かい精度の更新時刻を例に挙げ、値が不透明なのでクライアントがタグの構成を知る必要はないと述べています。ですからクライアントは、"v7"を見てバージョン番号だと推測し数字を増やして送ったり、ハッシュだと決めつけて自分で計算して比べたりしてはいけません。受け取った値をそのまま保存し、そのまま送り返すだけです。

同じ節は、変更を合理的かつ一貫して検出できる表現であれば、オリジンサーバーはETagを送るべき(SHOULD)だとしています。また、以前の定義ではクォートの中をquoted-stringとして扱っていたため、バックスラッシュをエスケープとして解釈する受信者がありうるので、サーバーはタグにバックスラッシュを使わないほうがよいという注意も付いています。

強いETagと弱いETag

RFC 9110の8.8.1節の定義は次のとおりです。

  • 強い検証子(strong validator): GETに対する200レスポンスの本文から観測できる表現データが変わるたびに値が変わります。
  • 弱い検証子(weak validator): 表現データが変わっても値が変わらないことがあります。1秒単位の時刻のように計算方法に限界がある場合や、サーバーが独自の基準で複数の表現を「同じもの」としてまとめたい場合に生じます。

8.8.3節によると、ETagはデフォルトで強い検証子で、生成方法が強い検証子の条件をすべて満たさない場合、オリジンサーバーは必ずW/を付けなければなりません(MUST)。8.8.1節は、gzipを適用した表現と適用していない表現に同じ検証子を送れば、その検証子は弱いものになるという例も挙げています。表現データが異なるからです。コンテンツネゴシエーションとETagの関係、レスポンスを選ぶキャッシュキーについては、HTTP Varyとキャッシュキーで別に扱いました。

二つのタグを比べる関数も二つあります(8.8.3.2節)。

  • 強い比較: 両方とも弱くなく、クォートの中の文字列が1文字ずつ一致すれば同じとみなします。
  • 弱い比較: W/の有無にかかわらず、クォートの中の文字列が1文字ずつ一致すれば同じとみなします。
タグ1 タグ2 強い比較 弱い比較
W/"1" W/"1" 不一致 一致
W/"1" W/"2" 不一致 不一致
W/"1" "1" 不一致 一致
"1" "1" 一致 一致

この表はRFC 9110の表3をそのまま訳したものです。実務で効いてくるのは1行目です。強い比較では、弱いタグ同士は文字列が同じでも一致しません。8.8.1節が、強い検証子はあらゆる条件付きリクエストに使えるが、弱い検証子はキャッシュエントリの検証のように以前の表現データと完全に同じである必要がない場合にしか使えない、とまとめているのはこのためです。

条件付きリクエストの二つの用途

RFC 9110の13節は、条件付きリクエストを、対象リソースにメソッドを適用する前に確かめる前提条件(precondition)をリクエストヘッダーで指定したリクエストと定義しています。同じ節は、条件付きGETがHTTPキャッシュを更新するもっとも効率的な仕組みであること、PUTやDELETEのような状態を変えるメソッドに条件を付ければ、並行して作業していた別のクライアントの成果をうっかり上書きしてしまう「lost update」問題を防げることを説明しています。

If-None-Match If-Match
主な用途 キャッシュの再検証(条件付きGET) 状態を変えるメソッドでの上書き防止
タグの比較 弱い比較 強い比較
条件が偽のとき GET・HEADは304、それ以外のメソッドは412 412、または同じ変更がすでに反映されているように見えれば2xx
評価する側 キャッシュまたはオリジンサーバー オリジンサーバー(キャッシュと仲介者は無視してよい)
*の意味 現在の表現がないときだけ真 現在の表現があるときだけ真

根拠はRFC 9110の13.1.1節、13.1.2節、13.2.2節です。以下で二つの場合を分けて見ていきます。

キャッシュの再検証: If-None-Matchと304

クライアントが最初に受け取ったやり取りが次のようだったとします。

GET /notes/42 HTTP/1.1
Host: api.example.com
Accept-Encoding: gzip
HTTP/1.1 200 OK
Content-Type: application/json
ETag: "v7"
Vary: Accept-Encoding

{"text": "draft"}

RFC 9111の4.3節によると、キャッシュは保存したレスポンスをそのまま使えないとき(たとえば新鮮でないとき)、条件付きリクエストで次のサーバーに判断を任せることができ、これを検証または再検証と呼びます。保存したレスポンスにエンティティタグがあれば、キャッシュは検証リクエストにそのタグを必ず含めなければなりません(MUST、4.3.1節)。RFC 9110の13.1.2節も、エンティティタグを持つ保存済みレスポンスを更新したいクライアントは、GETリクエストにそれらのタグを並べたIf-None-Matchを付けるべき(SHOULD)だとしています。

GET /notes/42 HTTP/1.1
Host: api.example.com
Accept-Encoding: gzip
If-None-Match: "v7"

If-None-Matchは弱い比較を使います(MUST、13.1.2節)。弱いタグもキャッシュの検証には使えるからです。並べたタグのどれかが選択された表現のタグと一致すれば条件は偽になり、オリジンサーバーはメソッドを実行せず、GET・HEADなら304を返さなければなりません(MUST)。

HTTP/1.1 304 Not Modified
Date: Tue, 13 Oct 2026 04:05:00 GMT
ETag: "v7"
Vary: Accept-Encoding

304には本文がありません。RFC 9110の15.4.5節は、304はヘッダーセクションの終わりで終わり、コンテンツもトレーラーも含められないと定めています。その代わり、同じリクエストに200を返していたら含めたはずのContent-Location、Date、ETag、Vary、Cache-Control、Expiresを送らなければなりません(MUST)。上の304の例にはDateが入っています。

304を受け取ったキャッシュは、更新対象の保存済みレスポンスを選び、そのヘッダーを304に含まれるヘッダーで更新してから再利用します(RFC 9111の4.3.3節、4.3.4節)。304に強い検証子があれば、同じ強い検証子を持つ保存済みレスポンスが更新対象で、そうしたレスポンスが一つもなければ、その304で何も更新してはなりません(MUST NOT)。逆に本文のある完全なレスポンスが返ってきたら、条件付きリクエストで指定した保存済みレスポンスはどれも使えないという意味なので、キャッシュはその完全なレスポンスでリクエストに応えなければなりません(MUST)。

304は「読み取りのリクエストについて、すでに持っている表現を使い続けてよい」というレスポンスです。書き込みが成功したことを伝えるステータスコードではありません。GET・HEAD以外のメソッドでIf-None-Matchが偽になれば、レスポンスは412です。

キャッシュが検証リクエストを送るときは、If-Modified-Sinceも一緒に付けることがよくあります。RFC 9111の4.3.1節は、エンティティタグのほうが明らかに優れていても、エンティティタグの条件を理解しない古い仲介者に対応するため、たいていは両方の検証子が送られると書いています。両方が届いたときの扱いは、後の評価順序で説明します。

同時更新の防止: If-Matchと412

二つのクライアントAとBが同じメモを編集する場面を、RFC 9110のルールどおりにたどってみます。実際に観測した障害ではなく、13節と13.1.1節の意味を順に当てはめた例です。

  1. AとBがそれぞれGET /notes/42を送り、どちらもETag: "v7"の表現を受け取ります。
  2. Aが編集した内容をIf-Match: "v7"付きでPUTします。現在のタグは"v7"なので条件は真で、サーバーは内容を保存します。現在のタグは"v8"になります。
  3. Bも自分の編集内容をIf-Match: "v7"付きでPUTします。
  4. サーバーは"v7"と現在のタグ"v8"を強い比較で照らし合わせ、条件が偽だと判断します。メソッドを実行せず、412を返します。

3のPUTを条件なしで送っていたら、Bの内容がAの変更を上書きし、Aはそれに気づく手段がありません。RFCがlost updateと呼んでいるのがこの状況です。

Aのリクエストとレスポンスです。

PUT /notes/42 HTTP/1.1
Host: api.example.com
Content-Type: application/json
If-Match: "v7"

{"text": "edited by A"}
HTTP/1.1 204 No Content
ETag: "v8"

PUTの成功レスポンスにETagを入れるには条件があります。RFC 9110の9.3.4節は、リクエストの内容を一切変換せずに保存し、その値が新しい表現を反映している場合でなければ、成功レスポンスにETagなどの検証子を送ってはならない(MUST NOT … unless)と定めています。保存時に内容を書き換えるサーバーなら、上のレスポンスからETagを外す必要があります。

Bのリクエストとレスポンスです。

PUT /notes/42 HTTP/1.1
Host: api.example.com
Content-Type: application/json
If-Match: "v7"

{"text": "edited by B"}
HTTP/1.1 412 Precondition Failed

412は、リクエストヘッダーで指定した条件の一つ以上がサーバー上で偽と評価されたことを示します(15.5.13節)。その後Bが何をするかはRFCの範囲外です。よくあるのは、もう一度GETして"v8"の表現を受け取り、ユーザーに競合を見せるか内容をマージしたうえで、If-Match: "v8"を付けて送り直すやり方です。

If-MatchについてRFC 9110の13.1.1節が定めていることをまとめると次のとおりです。

  • オリジンサーバーは強い比較を使わなければなりません(MUST)。クライアントはこの条件によって、表現データが少しでも変わっていればメソッドを適用させたくないからです。先ほどの比較表から導かれることとして、サーバーが弱いタグしか返さないなら、そのタグをそのままIf-Matchに入れても条件が真になることはありません。
  • 条件が偽なら、オリジンサーバーはメソッドを実行してはなりません(MUST NOT)。失敗を伝える方法として412を返してよく(MAY)、状態を変えるリクエストがすでに反映されているように見えるなら2xxを返してもかまいません(MAY)。RFCが挙げる例は、前のレスポンスが失われ、成功を知らないクライアントが同じリクエストを送り直した場合です。
  • この2xxの許可は効率的ですが、よく似ていても互いに協調しない変更リクエストが複数来る場合には危険があります。RFCは非アトミックなインクリメントのように共有リソースをセマフォとして使う例を挙げ、そうしたリソースでは安全でないメソッドの条件が失敗するたびに412を返すほうがよいとしています。
  • キャッシュや仲介者はIf-Matchを無視してかまいません(MAY)。この条件はオリジンサーバーでしか意味を持たないからです。
  • 値が*なら、オリジンサーバーに対象リソースの現在の表現があるとき真です。*とほかの値を一つのリストに混ぜるのは構文上誤りです。

ETagはロックではない

If-Matchはリソースをあらかじめ押さえておくロックではありません。リクエストが届いた時点の現在の状態を条件と比べるだけで、AがGETしてからPUTするまでの間、ほかのクライアントの書き込みを止めはしません。防ぐのは、古い状態を前提にした書き込みが適用されることです。

サーバーの実装にはもう一つ必要なことがあります。上の流れでAとBのPUTがほぼ同時に届くと、サーバーが両方のリクエストについて「現在のタグは"v7"」と確認してから、それぞれ保存してしまう可能性があります。そうなると二つの条件がどちらも真と評価され、結局一方がもう一方を上書きします。ですから、タグの比較と保存はサーバー内で一つのアトミックな処理として行う必要があります。これはRFCが保証している内容ではなく、RFCの意味を守るために実装側に求められることを推論したものです。RFC 9110の9.3.4節は、HTTPはリソースの状態をどう保存するかを定義しないと明記しています。データベースであればバージョン列を条件にしたUPDATEやトランザクションといった方法が考えられますが、どれを使うかはHTTPの外での選択です。

新規作成のときだけ許す: If-None-Match: *

If-None-Match: *は、オリジンサーバーに対象リソースの現在の表現があれば偽です(13.1.2節)。RFCはこれを、リソースにまだ表現がないとクライアントが考えているときに、PUTのような安全でないメソッドが既存の表現をうっかり変更しないようにする使い方として説明し、複数のクライアントが最初の表現を同時に作ろうとしたときに起こるlost updateの変形だとしています。

PUT /notes/43 HTTP/1.1
Host: api.example.com
Content-Type: application/json
If-None-Match: *

{"text": "first version"}
  • /notes/43に現在の表現がなければ条件は真で、PUTが表現を新しく作った場合、オリジンサーバーは201 Createdで知らせなければなりません(MUST、9.3.4節)。
  • 別のクライアントが先に作っていれば条件は偽で、メソッドがGET・HEADではないので412になります(MUST、13.1.2節)。

ここでも「存在の確認」と「作成」をサーバー内でアトミックに行う必要があるという点は、前の節と同じ実装側の推論です。

日付の条件と評価順序

最終更新日時による条件もあります。If-Unmodified-Sinceはクライアントがエンティティタグを持っていないときにIf-Matchと同じ目的で使い(13.1.4節)、If-Modified-Sinceはエンティティタグのないキャッシュ済み表現を更新するのに使われます(13.1.3節)。ETagの条件と一緒に来たときのルールは短いものです。

  • リクエストにIf-Matchがあれば、If-Unmodified-Sinceは無視しなければなりません(MUST、13.1.4節)。
  • リクエストにIf-None-Matchがあれば、If-Modified-Sinceは無視しなければなりません(MUST、13.1.3節)。GET・HEAD以外のメソッドに付いたIf-Modified-Sinceも無視しなければなりません。

RFCは、エンティティタグの条件は日付の条件をより正確に置き換えるものであり、両方を一緒に送るのはエンティティタグの条件を実装していない古い仲介者と相互運用するためだと説明しています。

条件ヘッダーが複数あるときは、RFC 9110の13.2.2節の順序で評価しなければなりません(MUST)。

順序 条件 評価する場合 真のとき 偽のとき
1 If-Match 受信者がオリジンサーバー 3へ 412。ただし状態の変更がすでに成功したと判断できる場合を除く
2 If-Unmodified-Since 受信者がオリジンサーバーでIf-Matchがないとき 3へ 1と同じ
3 If-None-Match あるとき 5へ GET・HEADは304、それ以外は412
4 If-Modified-Since GET・HEADでIf-None-Matchがないとき 5へ 304
5 If-Range GETでRangeと一緒に来たとき Rangeが適用できれば206 Rangeを無視して200
6 (なし) それ以外 メソッドを実行し、その結果どおりに応答

If-Rangeはこの記事の範囲外なので、表に載せるだけにしました。

評価のタイミングも決まっています。13.2.1節によると、キャッシュやオリジンサーバーは、通常のリクエストチェックを終えた後、リクエストの内容を処理するかメソッドの動作を行う直前に条件を評価しなければなりません(MUST)。また、条件を外した同じリクエストへのレスポンスが2xxでも412でもないステータスコードになっていたはずなら、サーバーは受け取った条件をすべて無視しなければなりません。大きな処理をせずにわかるリダイレクトや失敗のほうが、条件の評価より先だということです。

評価する主体についてのルールもあります。

  • 対象リソースのオリジンサーバーでもなく、そのリクエストのキャッシュとしても振る舞えないサーバーは、これらの条件ヘッダーを評価してはならず(MUST NOT)、リクエストを転送するならヘッダーも一緒に転送しなければなりません(13.2.1節)。
  • キャッシュにはIf-MatchとIf-Unmodified-Sinceは当てはまりません。キャッシュが保存済みの200や206レスポンスでリクエストに応えられる場合は、If-None-Matchなどの該当する条件を保存済みレスポンスの検証子で評価すべきです(SHOULD、RFC 9111の4.3.2節)。
  • CONNECT、OPTIONS、TRACEのように表現を選んだり変えたりしないメソッドに付いた条件ヘッダーは無視しなければなりません(13.2.1節)。

実装とテストで確認すること

  • クライアントはタグの値を解釈したり自分で作ったりせず、受け取った値をそのまま送り返しているか。
  • 強い検証子の条件を満たさない方法で作ったタグにW/を付けているか。gzipの表現と元の表現が同じ強いタグを共有していないか。
  • If-None-Matchの処理には弱い比較を、If-Matchの処理には強い比較を使っているか。RFCの表3の4組をテストデータに入れておくとよいでしょう。
  • 一致するIf-None-Match付きのGETは本文のない304で、同じ条件付きのPUTは412で終わるか。
  • 200レスポンスに含まれていたはずのETag、Vary、Date、Cache-Control、Expires、Content-Locationが304にも付いているか。
  • 古いタグを付けたIf-MatchのPUTが412を受け取り、リソースが変わっていないか。同じタグで二つのPUTを同時に送ったとき、成功するのは一つだけか。
  • 失敗したIf-Matchに2xxを許すなら、「すでに反映されているように見える」の判断基準ははっきりしているか。カウンターやインクリメントのように、似ているが協調しないリクエストが集まるリソースでは常に412を返しているか。
  • If-None-Match: *付きのPUTが、リソースがないときは201、あるときは412を受け取るか。
  • If-MatchとIf-Unmodified-Since、If-None-MatchとIf-Modified-Sinceを一緒に送ったとき、日付の条件が無視されるか。
  • 条件なしでもリダイレクトや412以外のエラーで終わるはずのリクエストに条件ヘッダーを付けたとき、そのリダイレクトやエラーのレスポンスがそのまま返るか。
  • PUTの成功レスポンスのETagは、保存されたとおりの新しい表現を指しているか。保存時に内容を変えるサーバーならETagを外しているか。

出典

確認日: 2026-10-13

ブログへ