ByteScope

304

HTTP 304 Not Modified の直し方

3xx リダイレクトIETF 標準RFC 9110

手元のコピーはまだ有効です。リクエストに検証子を添えて送り、サーバーが自分の持っているものと比べ、一致したので、ヘッダーだけを返してボディを意図的に省いた——それが 304 です。

304 が出ている理由と、やるべきこと

同じ 3 桁でも、リクエストのどちら側にいるかで別々の問題になります。自分に当てはまるブロックを読んでください。

curl で 304 を再現する

失敗した URL に対してそのまま実行してください。エラーページを表示せずにステータスだけを出すので、ブラウザが描画したものではなくサーバーが言ったことが見えます。

curl
curl -sS --etag-save /tmp/etag -o /dev/null https://example.com/style.css && curl -sS -D - -o /dev/null --etag-compare /tmp/etag https://example.com/style.css

1 本目のリクエストは、応答に付いてきた `ETag` をそのままファイルに保存します。2 本目はそれを `If-None-Match` として送り返します。これはブラウザのキャッシュが再検証でやることそのものなので、模擬ではなく本物の 304 です。`-D -` が 2 本目の応答ヘッダーを落とし、ステータス行が答えのすべてです。`304` ならタグが一致したということ。ここから 4 つ読み取ります。1 つ目、そもそも 1 本目の応答に `ETag` があったか。タグも `Last-Modified` も無ければ、その URL は永遠に再検証できません。訪問のたびにサイト全体を落とし直す症状の説明がこれです。2 つ目、304 は RFC 9110 が求めるとおりタグを繰り返しているか。3 つ目、`W/` が付くのは圧縮をネゴシエートしたときだけか。2 本目を `-H 'Accept-Encoding: gzip'` あり/なしで実行して、片方が 304 で片方が 200 なら、検証子はエンコーディングごとに違うということで、キャッシュのキーには `Vary: Accept-Encoding` が要ります。4 つ目、答えたのはどの層か。`server:` と `cf-ray` の有無が、自前のコピーを持つエッジが返した 304 なのか、オリジンが返した 304 なのかを教えます。日付ベースの経路を試すなら `-z` を使います。curl が `--time-cond` として説明しているもので、指定した日付やローカルファイルのタイムスタンプから `If-Modified-Since` を組み立て、先頭にダッシュを付けると `If-Unmodified-Since` に反転します。

304 が実際に意味していること

RFC 9110 §15.4.5 は 304 を、条件付きの GET または HEAD に対して、前提条件が偽にならなければ 200 を返していたはずの場合の答えと定義しています。デバッグ中に効いてくる規則がこの節に 2 つあります。304 は本文を運べません。ヘッダーフィールドのあとの最初の空行でメッセージが終わると決まっているので、304 にボディを書き足すミドルウェアは、バイトを無駄にしているのではなくメッセージの区切りを壊しています。そして 304 は、キャッシュが必要とする範囲で、200 なら付いていたはずのヘッダーフィールドを繰り返さなければなりません。RFC 9110 はそれを `Content-Location`・`Date`・`ETag`・`Vary` と名指ししているので、`ETag` の付いていない 304 はその時点で怪しいということになります。条件そのものは §13 の話です。`If-None-Match` はエンティティタグを弱い比較関数で比べ、`If-Modified-Since` は HTTP-date を比べます。§13.2.2 が優先順位を決めていて、`If-Modified-Since` が評価されるのは `If-None-Match` が無いときだけです。つまり両方を載せたリクエストの結果はタグだけで決まり、あなたの `Last-Modified` は議論に参加すらしません。この 2 つの検証子は切れ味も同じではありません。HTTP-date の分解能は 1 秒 (§5.6.7) なので、同じ秒に入った 2 回の編集は区別できません。一方エンティティタグはオリジンが決めたものが何であれそれであり、`W/` が付いていない限り強い検証子です。

304 と間違えられやすいコード

紛らわしいコード見分け方
200ネットワークパネルの 304 と JavaScript から見える 200 は、しばしば同じ 1 往復です。ブラウザの HTTP キャッシュが条件付きリクエストを出し、304 を受け取り、保存済みの 200 で `fetch()` を解決します。だからバイトが線を渡っていなくても、コードが観測するステータスは 200 です。アプリケーションのコードで 304 が見えたなら、その条件付きヘッダーを付けたのはあなた自身です。
412同じ「前提条件の不成立」でも、何をしようとしていたかでコードが変わります。GET で検証子が一致しなければ 304 です。使えるコピーをすでに持っているからです。状態を変えるメソッド——PUT に付けた `If-Match` が競争に負けた場合——なら 412 (Precondition Failed, RFC 9110 §15.5.13) です。再利用できるものは無く、書き込みを進めてはいけないからです。
no-cache304 のトラフィックを*生んでいる*のが `no-cache` です。保存は許すが再利用の前に再検証を要求するので、毎回問い合わせが飛び、その多くが 304 で返ります。`no-store` は保存自体を禁じるので、送る検証子がそもそも無く、受け取る 304 もありません。古い表示を直すつもりで `no-store` に手を伸ばすと、古いコピーと一緒に安上がりな再検証まで捨てることになります。
ETag強さの違いは実在し、それを気にするフィールドは限られます。`If-None-Match` は弱い比較関数を使うので、`W/"v1"` と `"v1"` は一致し、gzip で弱められたタグでも 304 は出ます。`If-Range` と `If-Match` は強い比較を使うので、同じ弱いタグは範囲リクエストを黙って無効にします。gzip するサーバーで、接続が切れた大きなファイルがゼロから落とし直しになるのはこれが理由です。
理由句
Not Modified
クラス
3xx リダイレクト
定義
RFC 9110
標準
IETF 標準

このサイトのツール

どれもブラウザ内で完結します。アップロードは発生しません。

関連するステータスコード

304 を調べているとき、ついでに読むことになりがちなコードです。

よくある質問

このステータスコードを出したのがどのサーバーかを知るには?

ページではなく応答ヘッダーを読んでください。curl -sS -o /dev/null -D - https://example.com/path が本文なしでヘッダーだけを出し、serverviacf-ray の 3 つで、答えたのがどの層かが分かります。cf-ray の値があれば応答を扱ったのは Cloudflare で、その値はサポートが聞いてくる ID です。エッジを完全に外して確かめたいなら、--resolve example.com:443:203.0.113.10 を付けて同じリクエストを投げ直します。元のホスト名と SNI を送りつつ、指定したオリジンのアドレスへ接続するので、答えが変われば、エッジとオリジンの言い分が食い違っているということです。

数字のあとの理由句(Not Found など)に意味はありますか?

ありません。分岐条件にしてはいけません。RFC 9112 はクライアントに理由句を無視するよう求めていますし、サーバーは自由に変えられますし、HTTP/2 と HTTP/3 にはそもそも理由句が存在しません。HTTP/1.1 では 404 Not Found として届くものが、HTTP/2 では素の 404 として届きます。このサイトが登録済みの理由句を載せているのは、それが検索される語であり HTTP/1.1 のログに出る文字列だからで、何かのソフトウェアがそれに依存しているからではありません。

このコードで失敗したリクエストは再試行すべきですか?

5xx と 429 は再試行してよく、4xx はしないでください。次に投げても中身は同じだからです。応答に Retry-After が付いていればそれに従ってください。RFC 9110 がまさにこのために定義していて、値は秒数でも日時でも構いません。付いていなければ、ジッター付きの指数バックオフと明確な上限を使い、しかも冪等なメソッドに限ってください。再試行された POST はカードに二重で課金しかねません。すでに失敗しているサーバーへの再試行の嵐は、短い障害を長い障害に変える典型的な経路です。

自分のサーバーが返すステータスコードは変えられますか?

変えられます。守るべき規則は 1 つだけ、そのコードが本当であること。応答を自動で読むもの——検索エンジン、監視、キャッシュ、クライアント側の再試行ロジック——はすべて数字だけで判断し、ページの中身は一切見ません。エラーページを 200 で返せば自分の監視から障害が隠れますし、無いページを 200 で返せばエラーが本文として索引されますし、単に不正なリクエストに 500 を返せば、当番の人をスタックの間違った半分へ送り込むことになります。

304 でまだ詰まっているなら、ステータスコード一覧をすべて見る。あるいは上に戻って、自分の立ち位置に向けて書かれたブロックを読んでください。