Webサイトの条件付きリクエスト検証|ETag・Last-Modified・304を照合する方法
Webページの更新後に、利用者へ古い本文が返っていないかを確認したいとき、レスポンスが200だったかだけでは不十分です。前回取得した表現を識別するETagやLast-Modifiedを保存し、同じURLと同じ要求条件で条件付きGETを送ると、本文を再送せずに済むケースと、更新後の本文を取得すべきケースを分けられます。
最初の200でETagとLast-Modified、要求に使ったURL・ヘッダー・本文を記録し、次のGETでIf-None-Matchを優先して検証します。検証子が一致した未変更の表現は304で本文を再利用し、古い検証子と一致しない更新後の表現は200と新しいETag・本文で返る、という対応を確認します。
以下は、HTTP仕様に沿った読み方と、管理対象のWebサイトで再現できる確認手順です。配信経路や管理範囲を整理する際は、Webサイト構築の基盤選びも参考になります。例では架空の https://example.com/docs/guide.html を使います。実際のサーバーへ検証を実施した結果を示すものではなく、対象サイトの所有者がログ、ヘッダー、本文を同じ条件で照合するための手順として読んでください。
本記事のポイント
- 最初の200でETag・Last-Modified・本文・要求バリアントを一組で保存し、同じURLと条件を再現してから304と200を比較します。
- If-None-Matchは日付条件に優先し、GETではETagの一致なら本文なしの304、不一致なら通常の200応答で新しい本文を取得する。
- no-cacheとno-storeを混同せず、VaryやCDNの状態、オリジンログを突き合わせて配信経路の差を証拠に基づいて切り分けます。
条件付きGETで照合する3つの情報
条件付きリクエストは、クライアントが以前受け取った表現を基準に「同じなら再送不要か」を尋ねる仕組みです。ETagは選択された表現を区別する不透明な識別子、Last-Modifiedはサーバーが判断した最終更新時刻です。レスポンスのETagを次の要求のIf-None-Matchへ、Last-ModifiedをIf-Modified-Sinceへコピーして使います。
| 確認対象 | 役割 | 判定時の注意 |
|---|---|---|
| ETag | 表現の版を区別する検証子 | 引用符を含む値をそのまま保存し、強い検証子か弱い検証子かを見る |
| Last-Modified | 表現の更新時刻を示す検証子 | HTTP-dateと時計の差、1秒単位の精度による見落としを考慮する |
| 304 | 保存済みの本文を再利用できることを示す応答 | 本文やトレーラーを持たず、保存済み応答の更新に使うヘッダーを返す |
ETagに W/ が付く場合は弱い検証子です。強い検証子は、GETの200応答で観測できる表現データが変わるたびに値が変わる必要があります。弱い検証子は、意味上同等と扱いたい表現をまとめることがあり、バイト単位の完全一致を表すとは限りません。If-None-Matchの比較では弱い比較が使われるため、W/"guide-v7" と "guide-v7" はキャッシュ検証上、一致し得ます。
Last-Modifiedは、サーバーが表現の更新日時を一貫して把握できる場合に有用ですが、HTTP-dateの精度や時計のずれによって、短時間に行われた複数更新を区別できないことがあります。ETagが提供されているなら、通常はETagを主な照合軸にし、Last-Modifiedは互換性と補助情報のために併記します。
最初の200でURLと表現バリアントを固定する
検証を始める前に、対象URLと表現の条件を固定します。ここでは全コマンドで同じ GET https://example.com/docs/guide.html を使い、Accept-Encoding: identity と Accept-Language: ja を送ります。クエリを毎回付け替えたり、途中でリダイレクト先を混ぜたりすると、キャッシュキーや選択される表現が変わるため、304と200の差を検証子だけの差として読めません。
まず本文を受け取る通常のGETを実行し、ヘッダーと本文を別ファイルへ保存します。ファイル名は例です。取得した時刻、実行したコマンド、HTTPステータス、URL、要求ヘッダー、CDN経由かどうかも一緒に記録してください。
curl -sS --max-time 30 \
-H 'Accept-Encoding: identity' \
-H 'Accept-Language: ja' \
-D first.headers \
-o first.body \
-w 'status=%{http_code}\n' \
'https://example.com/docs/guide.html'
ここで確認する最初の期待状態は、status=200、ETag: "guide-v7"、Last-Modified: Tue, 22 Sep 2026 09:00:00 GMT のような組み合わせです。値は架空の例なので、実際には取得した応答の値を使います。本文が本当に目的のHTMLかは、Content-Type、Content-Length、本文の先頭、必要ならハッシュで確認します。200でもエラーページやアクセス制限の画面なら、正しい表現を保存したことになりません。
同じURLでも、言語、Cookie、認証、デバイス、圧縮方式などで表現が変わることがあります。表現を選ぶ要求ヘッダーがある場合はVaryも保存し、2回目以降に同じ条件を再現します。特に圧縮も調べる場合は、まずidentityで検証子の流れを確認し、次にgzipやBrotliを別のバリアントとして扱います。圧縮された本文の復号とContent-Encodingの照合は、HTTP圧縮の検証手順の領域です。

If-None-Matchを優先して304を確かめる
この例は、固定した表現に強いETagを付け、本文を変更すると値も変わる構成を想定します。最初の応答にETagがあるなら、まずその値をIf-None-Matchへ送ります。Last-Modifiedも併記できますが、受信者はIf-None-Matchが含まれる要求ではIf-Modified-Sinceを無視し、ETag条件を優先します。これは、時刻よりも表現を区別しやすいETagを主条件にするためです。
curl -sS --max-time 30 \
-H 'Accept-Encoding: identity' \
-H 'Accept-Language: ja' \
-H 'If-None-Match: "guide-v7"' \
-H 'If-Modified-Since: Tue, 22 Sep 2026 09:00:00 GMT' \
-D unchanged.headers \
-o unchanged.body \
-w 'status=%{http_code}\n' \
'https://example.com/docs/guide.html'
表現が変わっていなければ、GETの条件が偽になった結果として304 Not Modifiedが返ることがあります。304は障害や本文欠落を表すステータスではありません。クライアントが持つ first.body を、200で受け取った本文と同じものとして使うための応答です。RFC 9110では304はヘッダー部の終わりで終了し、コンテンツやトレーラーを含めません。保存先ファイルの有無や空であることだけで判定せず、HTTPステータスと受信本文がないことを確認します。以前のファイルを見誤らないよう、検証ごとに未使用の保存先を用いてください。通常のcurlは保存済み本文を自動で合成して表示するブラウザーキャッシュではないため、304を得たら別途保存したfirst.bodyを参照します。
304では、200なら送られるETag、Vary、Cache-Controlなど、保存済み応答を更新するためのヘッダーが送られます。ETag、Last-Modified、Cache-Control、Ageなどを、最初の200と機械的に同じかだけでなく、キャッシュが更新に使う情報として記録します。304が返ったことだけから、どの層が判定したかやオリジンがどの処理を行ったかを断定しないでください。
ETagがないときは、If-Modified-Sinceだけを使った確認に切り替えます。
curl -sS --max-time 30 \
-H 'Accept-Encoding: identity' \
-H 'Accept-Language: ja' \
-H 'If-Modified-Since: Tue, 22 Sep 2026 09:00:00 GMT' \
-D date-check.headers \
-o date-check.body \
-w 'status=%{http_code}\n' \
'https://example.com/docs/guide.html'
この要求にIf-None-Matchを追加すると、日付条件は主判定になりません。テスト記録にも「ETagあり・両方送信」「ETagなし・日付だけ」を分けて書き、条件を混ぜないことが重要です。
更新後は古い検証子で200と新しい本文を確認する
次は、同じURLと同じ要求バリアントを保ったまま、サーバー側の表現が更新された場合を考えます。クライアントがまだ "guide-v7" と古い更新日時を持っている状態で、サーバーの現在値が ETag: "guide-v8"、Last-Modified: Tue, 22 Sep 2026 10:30:00 GMT になっているとします。先ほどと同じ条件付きGETをもう一度送ります。
curl -sS --max-time 30 \
-H 'Accept-Encoding: identity' \
-H 'Accept-Language: ja' \
-H 'If-None-Match: "guide-v7"' \
-H 'If-Modified-Since: Tue, 22 Sep 2026 09:00:00 GMT' \
-D updated.headers \
-o updated.body \
-w 'status=%{http_code}\n' \
'https://example.com/docs/guide.html'
古いETagが新しい表現へ一致しなければ、If-None-Matchの条件は成立し、GETでは304ではなく200の完全な応答が返ります。updated.headersのETagとLast-Modifiedが新しい値になり、updated.bodyに更新後の本文が入ることを確認します。両方の条件を送っていても、If-Modified-Sinceの日時から結果を決めるわけではありません。If-None-Matchが存在するため、まずETagの不一致を基準に判定します。
この3つの状態を一つの表にすると、判定を取り違えにくくなります。
| 段階 | 送る検証子 | 想定する応答 | 本文の扱い |
|---|---|---|---|
| 初回 | なし | 200、ETagとLast-Modifiedを受領 | 本文を保存する |
| 未変更 | 現在と一致するETag(必要なら日付も併記) | 304 | 保存済み本文を再利用する |
| 更新後 | 古いETagと古い日付 | 200、新しい検証子 | 新しい本文へ置き換える |
本文の比較では、改行や生成時刻のような動的な差が本当に更新を意味するかを確認します。固定ファイルならハッシュや差分を使えますが、個人化されたページでは利用者条件をそろえないと、検証子の結果と本文の差を一対一に結び付けられません。弱いETagが意味上の同等性を示す設計なら、バイト列が変わっただけで直ちに失敗とは限らないため、提供側の検証子の定義も確認します。
Cache-Controlと配信経路を分けて判断する
Cache-Control: no-cache と Cache-Control: no-store は似た名前でも目的が異なります。レスポンスのno-cacheは、保存した応答を次の要求へ使う前に、オリジンなどへ転送して検証することを求めます。保存そのものを禁止する指示ではありません。一方、レスポンスのno-storeは、その要求や応答の一部をキャッシュへ保存せず、別の要求を満たすためにも使わない指示です。プライバシーを完全に保証する仕組みではない点も、運用上の説明に含めます。
要求側の Cache-Control: no-cache は、手元の保存応答を検証なしで使わないという希望を示します。no-store は、要求と応答を保存しないよう求める指示です。要求にno-storeを付けても、既に保存されていた応答への適用や削除を意味するわけではありません。またno-cacheを送った事実だけではオリジンへの到達を証明できないため、ログと照合します。何を確かめる要求かを分けて記録してください。
CDNとオリジンのどちらで条件が評価されたかを調べるときは、同じURL、同じ要求ヘッダー、同じ時刻情報を保った比較を行います。Age、Via、X-Cache、CDN固有のキャッシュ状態、アクセスログの到達記録などを並べ、許可されたオリジン直結の検証環境がある場合だけ別経路として確認します。CDNで304だったことだけ、または一つのヘッダーだけから、オリジンの不具合やCDNの誤配信を断定してはいけません。キャッシュキー、Vary、再検証の転送、ヘッダーの書き換えを配信事業者の仕様と照合します。
Content-Encodingを変えるテストでは、圧縮方式自体が別の表現になることがあります。gzip圧縮済みの表現と非圧縮の表現のようにデータが異なる場合、同じ強いETagを共有してはいけません。Vary: Accept-EncodingやETagの生成規則と合わせて判断します。検証子のテストではidentityを固定し、圧縮のテストではgzipやBrotliごとにURL・要求条件・レスポンスを別記録にすると、キャッシュ検証と圧縮検証を混同しません。
実務では、次の順で証拠を残すと切り分けやすくなります。
- 初回200の全ヘッダー、本文、URL、要求ヘッダー、取得時刻を保存する。
- 同じURLと同じバリアントで、一致するETagを送り、304のステータスと本文なしを確認する。
- 表現を更新した後、古いETagを送り、200、新しいETag、更新後本文を確認する。
- CDNの状態、オリジンログ、Vary、Cache-Controlを突き合わせ、どの層で差が生じたかを推測ではなく証拠で絞る。
本文の更新前後で古い検証子を送り、未変更の304と更新後の200を対にして確かめます。
よくある質問
304で本文が返らないのはエラーですか?
エラーではありません。304は条件付きGETまたはHEADで、クライアントが有効な保存済み表現を持っていると判断されたときに、同じ本文を再送しないための応答です。クライアントは保存済みの200本文を使います。304の応答自体へ本文を付けることを前提にした確認は行いません。
If-None-MatchとIf-Modified-Sinceを両方送った場合、どちらを見ますか?
If-None-Matchが優先されます。If-None-Matchが存在する要求では、受信者はIf-Modified-Sinceを無視します。互換性のため両方を送る実装はありますが、テスト記録にはETagを主条件として評価したことを残します。
Last-Modifiedだけでキャッシュ検証できますか?
できますが、時刻の精度や時計のずれに注意が必要です。短時間に複数回更新される表現や、更新日時を一貫して取得できない構成では、ETagの方が版を区別しやすい場合があります。ETagがない場合にIf-Modified-Sinceを使い、日付の出所と精度を記録します。
強いETagと弱いETagはどう使い分けますか?
強いETagは本文など200応答で観測できる表現データの変更を区別する検証子です。弱いETagは、サーバーが意味上同等と扱う表現をまとめるために使われることがあります。キャッシュ検証のIf-None-Matchは弱い比較を使うため、弱いETagをバイト単位の完全一致だと解釈しないでください。
no-cacheなら応答は保存されませんか?
必ずしもそうではありません。no-cacheは、保存した応答を検証なしで再利用しない指示です。保存自体を禁止するno-storeとは別です。利用者ごとの情報や保存禁止要件がある場合は、対象の要求・応答にno-storeが適切か、認証やCookieを含めて設計します。
CDNから304が返れば、オリジンも正常だといえますか?
その一つの結果だけでは断定できません。CDNの保存応答が条件に一致して304を返す場合があり、オリジンへ再検証が転送されたかは別の確認が必要です。同じURLと要求条件でCDNのヘッダー、オリジンのログ、再検証時刻を照合します。
ETagやCDNの設定が複数箇所に分かれ、更新後の表示や再検証を一貫して確認できない場合は、対象URL・配信経路・切り分け手順を整理するところから始められます。ファネルAiへ、Web基盤の設計と運用改善をご相談ください。