Dev Study
← HTTPとTLSの仕組み
レッスン 4 / 12·約 8 分·第18期 HTTPとTLSの仕組み

条件付きリクエストは、比べる役をサーバに渡す

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力今ここ
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口今ここ
  6. アプリ処理
  7. 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.

If-None-Match: 札が一致すれば本文を送らない
ブラウザ前の本文と ETag を保存している
If-None-Match で札をそのまま送り返す
サーバ今の ETag と比べる(弱い比較)
比べた結果
一致304。ヘッダだけで本文なし
不一致200。新しい本文と新しい ETag
  • ・クライアントは札の中身を解釈しない。比べるのはサーバ側(今の版を最終的に決めるのはオリジンサーバ)
  • ・札を共有していれば本文の転送を省ける。共有が崩れていれば全文を送り直す
具体実物

nginx が返した札と 304

nginx 1.31.6(nginx:alpine)で、11バイトの静的ファイルを GET し、返ってきた ETag を If-None-Match に入れてもう一度 GET しました。2回とも同じ結果です。以下は1回目の出力の抜粋(先頭から 304 の ETag 行まで。その範囲は省略なし)で、途中の ETag captured: 行は実演スクリプトが控えた値を表示したものです。

curl -si で2回 GET(2回目は If-None-Match 付き)。run 1 の抜粋コマンド出力
HTTP/1.1 200 OK
Server: nginx/1.31.6
Date: Thu, 24 Sep 2026 01:59:21 GMT
Content-Type: text/html
Content-Length: 11
Last-Modified: Mon, 31 Aug 2026 15:00:00 GMT
Connection: keep-alive
ETag: "6a9596f0-b"
Accept-Ranges: bytes
 
hello etag
ETag captured: "6a9596f0-b"
HTTP/1.1 304 Not Modified
Server: nginx/1.31.6
Date: Thu, 24 Sep 2026 01:59:21 GMT
Last-Modified: Mon, 31 Aug 2026 15:00:00 GMT
Connection: keep-alive
ETag: "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/ の付いた札はどれとも一致しません。

条件が偽だったときの縛りの強さも違います。

条件が偽のとき(RFC 9110 §13.1.1・§13.1.2)表
ヘッダ 実行 返す応答
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 と B が同じ版を読んでから書く(run 1)コマンド出力
# A reads
etag: "v1"
# B reads
etag: "v1"
# A writes with If-Match "v1"
HTTP/1.1 200 OK
etag: "v2"
saved
# B writes with If-Match "v1" (stale)
HTTP/1.1 412 Precondition Failed
etag: "v2"
precondition failed
# weak tag W/"v2"
412
# final GET
A edit

B の書き込みは 412 で止まり、最後の GET には A の内容が残りました。W/"v2" が 412 になったのは、このサーバが強い比較を実装したからです。412 を返すのもこのコードの選択で、製品一般の挙動ではありません。

読んでから書くまでの間に更新が消える問題そのものは、後で読む「データの入口」コースの「原子性は割り込みを防がない — 読んでから書く間に消える更新」で扱います。

✓ ここまでの確認

PUT に付けた If-Match が今の ETag と一致しなかったとき、RFC 9110 がオリジンサーバに課していることとして正しいのはどれですか。

⇄ 2つを見比べる
具体例 A

GET + If-None-Match(nginx 1.31.6)

札が今の版と一致したので 304 が返り、本文は送られませんでした。弱い比較で十分なのは、キャッシュの写しが意味の上で同じなら使えるからです。

実測表
送った札 今の札 結果
"6a9596f0-b" "6a9596f0-b" 304(本文なし)
"6a9596f0-b" "6a96e870-e" 200(全文)
具体例 B

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。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

環境:

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 3正解: 0

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)。