目次
変わっていないJSONを何度もダウンロードするAPIでは、HTTPの条件付きGETが役立ちます。前に受け取った表現を示すETagをクライアントが送り、現在も同じならサーバーは本文のない304 Not Modifiedを返します。データを再利用する判断を、独自の「更新あり」APIではなくHTTPの仕組みに乗せられます。
本稿では公開された固定JSONを返すNode.jsの小さなサーバーを作ります。2026年10月24日にRFCと公式API文書を確認した実装解説です。実際の業務データへ広げるときは、表現の作り方とキャッシュ対象を追加で設計してください。
スポンサーリンク
ETagは期限ではなく表現の識別子
ETagは更新日時そのものではなく、選択された表現を識別する値です。レスポンスでETag: "abc"を受け取ったクライアントは、次のGETにIf-None-Match: "abc"を付けます。一致すれば304、不一致なら新しい本文とETagを伴う200が返ります。リクエスト自体がなくなる仕組みではありません。
RFC 9110のIf-None-Matchでは、照合に弱い比較を使うこと、GETとHEADで条件が一致したときに304を返すことが定義されています。ETagの文字列には二重引用符が必要です。W/という接頭辞があれば弱い検証子になります。
no-cacheとno-storeを使い分ける
no-cacheは保存そのものの禁止ではなく、再利用前の検証を要求します。no-storeはキャッシュへ保存させない指示です。条件付きGETを試す公開APIなら、保存したデータを毎回検証するpublic, no-cacheという組み合わせを使えます。更新を一定時間許容できるならmax-ageを設定する設計もありますが、その間は再検証されない点を受け入れる必要があります。
個人別データへpublicをコピーしてはいけません。privateで共有キャッシュから除外するか、保存自体を許さない要件ならno-storeを選びます。キャッシュ方針の意味はRFC 9111のレスポンス指示で確認できます。ETagを付けることと、誰が保存してよいかを決めることは別の設計です。
スポンサーリンク
Node.jsで公開JSONを返す
import { createServer } from 'node:http';
import { createHash } from 'node:crypto';
// 説明用の固定データ。本番では保存先から取得する。
const body = Buffer.from(JSON.stringify({ message: 'Hello' }));
const etag = '"' + createHash('sha256').update(body).digest('hex') + '"';
function matches(header, current) {
if (!header) return false;
if (header.trim() === '*') return true;
// entity-tag内のカンマを壊さないようタグ単位で抽出する。
const tags = header.match(/(?:W\/)?"[\x21\x23-\x7e\x80-\xff]*"/g) ?? [];
return tags.some(tag => tag.replace(/^W\//, '') === current);
}
createServer((req, res) => {
if (req.url !== '/api/greeting') {
res.writeHead(404).end(); return;
}
if (req.method !== 'GET' && req.method !== 'HEAD') {
res.writeHead(405, { Allow: 'GET, HEAD' }).end(); return;
}
const headers = {
'Content-Type': 'application/json; charset=utf-8',
'Cache-Control': 'public, no-cache',
ETag: etag
};
if (matches(req.headers['if-none-match'], etag)) {
res.writeHead(304, headers).end(); return;
}
res.writeHead(200, { ...headers, 'Content-Length': body.length });
res.end(req.method === 'HEAD' ? undefined : body);
}).listen(3000, '127.0.0.1');
このコードをserver.mjsとして保存し、node server.mjsで起動します。127.0.0.1:3000だけで待ち受ける検証用です。JSONを一度Bufferへ変換し、その同じバイト列のSHA-256を強いETagにしています。ハッシュの使い方はNode.js公式のcreateHashを参照できます。
If-None-Matchには複数タグや弱いタグが来るため、単純な文字列一致だけでは不足します。例では引用符で囲まれたタグを抽出してW/を外し、現在のタグと比較します。タグ内部のカンマを壊すため、単純なsplit(",")は避けています。無効なヘッダー全体を厳密に検証するパーサーではありません。本番ではHTTP条件評価を提供する実績あるフレームワークやミドルウェアを優先し、正常なヘッダーへの挙動を試してください。
存在する表現に対する*も扱います。HEADではGETと同じメタデータを返し、本文だけ送らない構成です。304のときもETagとCache-Controlを付け、保存済みデータの再利用条件を伝えます。304の本文へ空のJSONを返す処理は不要です。
curlで200と304を確かめる
curl -i http://127.0.0.1:3000/api/greeting
# 返されたETagを引用符ごと貼り付ける
curl -i -H 'If-None-Match: "ここに返された値"' http://127.0.0.1:3000/api/greeting
curl -I http://127.0.0.1:3000/api/greeting
初回は200とJSON、同じタグなら304で本文なしです。データのmessageを変えて再起動し、以前のタグを送ると200になることも確認します。ブラウザー経由では、ネットワークパネルで304を受けてもアプリが見るレスポンスはキャッシュ済み本文と統合されて見える場合があります。HTTPの転送確認とJavaScriptからの見え方を分けて調べましょう。
スポンサーリンク
圧縮やCDNと二重管理しない
サンプルは圧縮も表現の出し分けも行いません。gzipなどで本文を変えるプロキシを挟む場合、同じ強いETagを異なるバイト列へ流用してはいけません。圧縮ごとに検証子を変える、プロキシに生成を任せる、適切な弱いETagを使うなど、生成の責任を一か所へ決めます。言語やメディアタイプで本文が異なる場合も、選択された表現ごとにタグを管理します。
Varyは、Accept-EncodingやAccept-Languageなど、どのリクエストヘッダーで表現を分けるかを伝えるためのものです。ETagがあってもキャッシュキーの設計は不要になりません。CDNへ任せたつもりでオリジンも独自のETagを書き換えると、毎回200になったり意図しない再利用になったりするので、境界ごとにレスポンスヘッダーを観察します。
本文転送が減ってもDB負荷は残り得る
JSONを組み立ててからハッシュを計算する実装では、304でもDB取得とシリアライズを実行することがあります。削減できるのは主に本文の転送です。サーバー側の処理も減らしたいなら、表現の変更に確実に追従するリビジョンを先に取得し、変更があるときだけ本文を生成する構成を検討します。
最大更新日時だけでタグを作ると、削除、並び替え、関連テーブル更新、表示ロジック変更を取り逃がす可能性があります。リビジョンがどの変更で進むかを定義し、本文生成コードの変更時も無効化できる設計にしてください。誤った304は新しいデータを利用者へ届けない不具合になります。
導入後に見る指標と限界
- 同じタグ、異なるタグ、複数タグ、W/付きタグ、*を送り、200と304が意図どおり分かれるか。
- HEADと304の本文が空で、ETagやキャッシュ方針が維持されているか。
- 304比率と転送バイト数に加え、DB時間とCPU時間も確認する。
- プロキシ経由とオリジン直結で、圧縮・Vary・ETagに矛盾がないか。
ETagは条件付き更新のIf-Matchにも使われますが、本稿の再検証とは目的が異なります。同じタグがあるという理由だけで更新競合の対策まで完成したとは扱わず、まずGETの転送削減として小さな公開エンドポイントから導入しましょう。






