- ブラウザURL入力今ここ
- DNS名前→IP
- ルータ経路選択
- ロードバランサ振り分け
- Nginx受け口今ここ
- アプリ処理
- DB保存
前回は、リダイレクトで依頼をクライアントが作り直すことを見ました。今回は、手元の写しがまだ最新かを、ブラウザとサーバがどう確かめているかを見ます。
まず結論
通信の仕組みは、両端が何を共有しているかで決まります。ここで共有するのは、表現の版につけた札(ETag)です。同じ札を両端が持っていれば、本文の転送を省けます。
クライアントは前に受け取った札を送り返すだけで、札の中身は解釈しません。今の版と比べるのはサーバです。同じ仕組みを書き込みに向けると、他人の更新を古い内容で上書きする事故を止められます。
nginx から受け取った `ETag: "6a9596f0-b"` を `If-None-Match` に入れて、ファイルを変えないまま同じ URL を GET し直しました。何が返るでしょうか。
当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。
札を送り返し、比べるのはサーバ
「Webを支えるソフトウェア」コースの「キャッシュはどこに効くのか — 鍵と鮮度」で、失効したキャッシュは捨てられたのではなく、検証に成功すれば復活できると見ました。この回で扱うのは、その検証の手段です。
RFC 9110 §13 は、条件付きリクエストの用途を1段落に2つ並べています。
Conditional GET requests are the most efficient mechanism for HTTP cache updates [CACHING]. Conditionals can also be applied to state-changing methods, such as PUT and DELETE, to prevent the "lost update" problem: one client accidentally overwriting the work of another client that has been acting in parallel.
どちらの用途でも、比べる役はサーバにあります(§13.1)。
Each precondition defined below consists of a comparison between a set of validators obtained from prior representations of the target resource to the current state of validators for the selected representation (Section 8.8).
比べるのは「前に受け取った検証子」と「今の検証子」です。今の版を最終的に決めるのはオリジンサーバで、クライアントではないので、クライアントは前の札を送り返し、比べる役はサーバが持ちます。札の作り方もサーバが決めます(§8.8.3.1)。
Since the value is opaque, there is no need for the client to be aware of how each entity tag is constructed.
- ・クライアントは札の中身を解釈しない。比べるのはサーバ側(今の版を最終的に決めるのはオリジンサーバ)
- ・札を共有していれば本文の転送を省ける。共有が崩れていれば全文を送り直す
nginx が返した札と 304
nginx 1.31.6(nginx:alpine)で、11バイトの静的ファイルを GET し、返ってきた ETag を If-None-Match に入れてもう一度 GET しました。2回とも同じ結果です。以下は1回目の出力の抜粋(先頭から 304 の ETag 行まで。その範囲は省略なし)で、途中の ETag captured: 行は実演スクリプトが控えた値を表示したものです。
HTTP/1.1 200 OKServer: nginx/1.31.6Date: Thu, 24 Sep 2026 01:59:21 GMTContent-Type: text/htmlContent-Length: 11Last-Modified: Mon, 31 Aug 2026 15:00:00 GMTConnection: keep-aliveETag: "6a9596f0-b"Accept-Ranges: byteshello etagETag captured: "6a9596f0-b"HTTP/1.1 304 Not ModifiedServer: nginx/1.31.6Date: Thu, 24 Sep 2026 01:59:21 GMTLast-Modified: Mon, 31 Aug 2026 15:00:00 GMTConnection: keep-aliveETag: "6a9596f0-b"
304 には Content-Length も本文もありません。§15.4.5 は it cannot contain content or trailers と定め、304 を「保存済みの表現を、200 の本文だったかのように使え」と差し向ける応答だと説明しています。
札の 6a9596f0-b はハッシュではありません。nginx のソースは "%xT-%xO" で組み立てており、ファイルの更新時刻 1788188400 の16進が 6a9596f0、長さ 11 の16進が b です。公式文書の etag ディレクティブは、形式について何も書いていません。
ファイルを14バイトの hello etag v2 に書き換えて古い札を送ると、200 と全文、新しい札 "6a96e870-e" が返りました。札の共有が崩れたので、転送は省けません。
nginx の ETag `"6a9596f0-b"` を受け取ったクライアントがすべきことはどれですか。
同じ札で、書き込みを止める
札を送り返してサーバに比べさせる仕組みは、書き込みにも使えます。向きは逆で、If-None-Match は「札が違えば実行する」、If-Match は「札が同じときだけ実行する」です。
比べ方も変わります。§13.1.2 は If-None-Match に弱い比較を、§13.1.1 は If-Match に強い比較を、どちらも MUST で課しています。
An origin server MUST use the strong comparison function when comparing entity tags for If-Match (Section 8.8.3.2), since the client intends this precondition to prevent the method from being applied if there have been any changes to the representation data.
キャッシュの検証は意味が同じなら十分ですが、上書きを防ぐには少しの変化でも止めなければなりません。強い比較では W/ の付いた札はどれとも一致しません。
条件が偽だったときの縛りの強さも違います。
ヘッダ 実行 返す応答If-None-Match MUST NOT GET/HEAD は 304、他は 412(MUST)If-Match MUST NOT 412 で知らせてよい(MAY)適用済みに見えるなら 2xx でもよい(MAY)
If-Match で「実行しない」は MUST ですが、「412 を返す」は MAY です。
古い札の PUT は 412 で止まる
nginx の静的配信では PUT を試していないため、この実演用に Node v24.21.0 で25行のサーバを書きました。版番号を ETag "vN" にし、If-Match が今の ETag と文字列で一致したときだけ PUT を受け付けます。W/ 付きは一致させません(強い比較)。
A と B が同じ "v1" を読み、A が先に書き、B が古い "v1" のまま書きます。表示はステータス行・ETag 行・本文の最終行だけに絞っています(2回とも同じ結果)。
# A readsetag: "v1"# B readsetag: "v1"# A writes with If-Match "v1"HTTP/1.1 200 OKetag: "v2"saved# B writes with If-Match "v1" (stale)HTTP/1.1 412 Precondition Failedetag: "v2"precondition failed# weak tag W/"v2"412# final GETA edit
B の書き込みは 412 で止まり、最後の GET には A の内容が残りました。W/"v2" が 412 になったのは、このサーバが強い比較を実装したからです。412 を返すのもこのコードの選択で、製品一般の挙動ではありません。
読んでから書くまでの間に更新が消える問題そのものは、後で読む「データの入口」コースの「原子性は割り込みを防がない — 読んでから書く間に消える更新」で扱います。
PUT に付けた If-Match が今の ETag と一致しなかったとき、RFC 9110 がオリジンサーバに課していることとして正しいのはどれですか。
GET + If-None-Match(nginx 1.31.6)
札が今の版と一致したので 304 が返り、本文は送られませんでした。弱い比較で十分なのは、キャッシュの写しが意味の上で同じなら使えるからです。
送った札 今の札 結果"6a9596f0-b" "6a9596f0-b" 304(本文なし)"6a9596f0-b" "6a96e870-e" 200(全文)
PUT + If-Match(Node の自作サーバ)
札が今の版と違ったので書き込みは実行されず、412 が返りました。強い比較を使うのは、1バイトの違いでも他人の更新を消しうるからです。
送った札 今の札 結果"v1" "v1" 200(A の書き込み)"v1" "v2" 412(B は止まる)
片方は転送を省き、片方は上書きを止めています。2つに共通する仕組みは何でしょうか。
札を解釈しているのはどちらの端か、今の版を決めているのはどちらの端かを見てください。
この章のまとめ
- 両端が同じ札(ETag)を持っていれば、本文の転送を省ける。札が変われば全文を送り直す。
- クライアントは札を解釈せず送り返すだけ。今の版と比べるのはサーバ(§13.1、§8.8.3.1)。
- nginx 1.31.6 の静的ファイルの札は更新時刻と長さの16進で、ハッシュではない。公式文書は形式を書いていない。
- If-None-Match は弱い比較で、偽なら GET/HEAD は 304(MUST)。304 は本文を持てない。
- If-Match は強い比較で、偽なら実行しない(MUST NOT)。412 は MAY で、適用済みに見えるなら 2xx も MAY。
自分の現場で確かめる
読んだだけでは身につきません。手元の環境で実際に見てみてください。
理解度チェック
読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。
If-None-Match と If-Match は、ETag をどちらの比較関数で比べると RFC 9110 に定められていますか。
用語集(4語)
- 条件付きリクエスト
- メソッドを適用する前にサーバが確かめる前提条件をヘッダで付けた要求(RFC 9110 §13)。GET ならキャッシュの更新、PUT・DELETE なら失われた更新の防止に使う。
- ETag(エンティティタグ)
- 表現の版を見分ける不透明な札(§8.8.3)。作り方はサーバが決め、クライアントは解釈しない。`W/` が付くと弱い検証子。
- 強い比較・弱い比較
- 強い比較はどちらも `W/` なしで文字列が一致したときだけ等しい。弱い比較は `W/` を無視して文字列を比べる(§8.8.3.2)。
- 412 Precondition Failed
- 要求ヘッダの条件がサーバで偽になったことを示す(§15.5.13)。
出典(一次ソース)
- RFC 9110 HTTP Semantics — §8.8 Validator Fields ↗
- RFC 9110 — §13 Conditional Requests(§13.1.1 If-Match・§13.1.2 If-None-Match・§13.2.2) ↗
- RFC 9110 — §15.4.5 304 Not Modified ↗
- RFC 9110 — §15.5.13 412 Precondition Failed ↗
- nginx documentation — ngx_http_core_module(etag ディレクティブ) ↗
- nginx source — src/http/ngx_http_core_module.c(ngx_http_set_etag。master、2026-09-24 取得) ↗