Web APIを作ったり使ったりしていると、「どうすればもっと通信を速く、スマートにできるだろう?」と考える瞬間がやってきますよね。特に、何度アクセスしても中身がほとんど変わらないようなデータを、毎回フルサイズでダウンロードさせるのは、サーバーにとってもクライアントにとってもちょっぴりもったいない話です。
今回は、そんな無駄な通信をごっそり削ぎ落とし、Webの世界を軽やかにしてくれる「ETagとIf-None-Matchによる条件付きリクエスト」の世界へご案内します。ネットワークの裏側でパケットたちがどんなやり取りをしているのか、身近な例えを交えながら一歩ずつ紐解いていきましょう!
—
1. 毎回の全データ取得は、本当に必要ですか?
私たちが普段何気なく見ているWebサイトやスマホアプリは、裏側でAPIを通じて膨大なデータをやり取りしています。例えば、お気に入りのカフェのメニュー一覧を取得するAPIを想像してみてください。
メニューのデータは1日に何回も書き換わるものではありませんよね。それなのに、アプリを開くたびにサーバーが「これが最新のメニュー全データです!」と、何メガバイトもあるデータを毎回送り返してきたらどうなるでしょう?
サーバーのCPUは忙しくなり、モバイル回線のパケット(ギガ)はあっという間に消費され、画面が表示されるまでの待ち時間(レイテンシ)も伸びてしまいます。
ここでインフラエンジニアやAPIデザイナーが頭を悩ませるわけです。「すでに相手の手元にあるデータと同じなら、データをまるごと送る必要はないんじゃないか?」と。
—
2. 郵便配達で例える「ETag」と「条件付きリクエスト」の仕組み
この問題を鮮やかに解決してくれるのが、HTTPヘッダーの主役である ETag(イータグ)と If-None-Match(イフ・ナン・マッチ)です。難しい用語はいったん脇に置いて、現実世界の「手紙のやり取り」に置き換えて考えてみましょう。
ETag(Entity Tag)= 文書の「合言葉(指紋)」
サーバーにあるデータ(リソース)に対して、内容が書き換わるたびに変わる「一意のハッシュ値(指紋のようなもの)」を割り振ります。これが ETag です。
例えば、メニューデータが更新されるたびに、サーバーは \"v1.2.3\" のような合言葉をそのデータにペタッと貼り付けます。
If-None-Match = 「この合言葉と同じなら、中身ちょうだい!」
クライアント(ブラウザやアプリ)は、以前取得したデータの ETag(合言葉)を大事にメモしておきます。次に同じデータが欲しいとき、クライアントはサーバーへこう伝えます。
> 「ねえ、今手元にあるデータの合言葉は \"v1.2.3\" なんだけれど、サーバー側の最新データもこの合言葉のままかな? もしこの合言葉に一致しない(If-None-Match) なら、最新のデータを送ってよ!」
サーバーの判断と「304 Not Modified」
このお伺いを受けたサーバーの動きはとってもスマートです。
1. 現在の最新データの合言葉を確認します。
2. もし合言葉が \"v1.2.3\" のままで、データが書き換わっていなければ……サーバーはこう答えます。
> 「おっ、中身は変わっていないね! データ本体は送らないから、手元のキャッシュをそのまま使って!」
このとき、HTTPのステータスコードとして返されるのが 304 Not Modified です。データ本体(ボディ)は空っぽなので、通信量はほんのわずか。ネットワークの帯域幅を劇的に節約できるというわけです。
—
3. 実際のHTTP通信のやり取りを覗いてみよう
一連の流れがイメージできたところで、実際のHTTPパケットがどのような会話をしているのか、シーケンスの裏側を覗いてみましょう。
初回リクエスト(データがまだ手元にないとき)
クライアントがはじめてAPIにアクセスします。
GET /api/v1/menu HTTP/1.1
Host: api.example.com
サーバーのレスポンス:
サーバーはデータ本体とともに、合言葉である ETag を添えて返します。
HTTP/1.1 200 OK
Content-Type: application/json
ETag: "abc123xyz"
{
"shop": "Coffee Roasters",
"items": [...]
}
クライアントはこのとき、受信したデータと一緒に ETag: \"abc123xyz\" も大切に保存(キャッシュ)しておきます。
2回目以降のリクエスト(条件付きリクエスト)
しばらく経って、クライアントがもう一度同じメニュー画面を開きました。今度は If-None-Match を添えてリクエストを送ります。
GET /api/v1/menu HTTP/1.1
Host: api.example.com
If-None-Match: "abc123xyz"
サーバーのレスポンス(変更がない場合):
サーバーは「お、合言葉はまだ一致しているな」と判断し、データ本体を送らずにステータスコードだけを返します。
HTTP/1.1 304 Not Modified
ETag: "abc123xyz"
これにより、重たいJSONデータの送信が丸ごとスキップされ、ミリ秒単位の高速化とサーバー負荷の軽減が同時に達成されます。
—
4. 実装における注意点とベストプラクティス
「なるほど、じゃあ毎回ハッシュを計算して返せば完璧だね!」と思ったそこのあなた、素晴らしい着眼点です。しかし、実務の現場ではいくつか気をつけるべきポイントがあります。一歩ずつ確認していきましょう。
1. ETagの生成コストに気を付ける
毎回データベース全体の文字列からハッシュ(MD5やSHA-256など)を計算していると、逆にサーバーのCPU負荷が高くなって本末転送になってしまうことがあります。
- 対策: レコードの「最終更新日時(updated_at)」や「バージョン番号」を組み合わせるなど、低コストで一意性を担保できる仕組みを工夫しましょう。
2. 強いETag(Strong ETag)と弱いETag(Weak ETag)
HTTP仕様書には、ETagに2種類のタイプが存在します。
- 強いいETag: バイト単位で完全に一致していることを示します(例:
"abc123xyz")。 - 弱いETag: 中身のセマンティクス(意味)は同じですが、フォーマットや広告バナーの挿入などでわずかな違いがある場合に使われます(例:
W/"abc123xyz")。
APIのデータ連携においては、基本的には完全一致を保証する「強いETag」を使うのが一般的です。
—
5. バックエンド実装例(Node.js / Express)
最後に、実際のWebアプリケーションサーバーでどのようにこの仕組みを扱っているのか、Node.js(Express)を例に軽くコードを覗いてみましょう。
const express = require('express');
const crypto = require('crypto');
const app = express();
app.get('/api/v1/menu', (req, res) => {
// 1. 取得したデータのモック(本来はDBから取得)
const menuData = {
shop: "Coffee Roasters",
updated_at: "2023-10-25T10:00:00Z",
items: ["Espresso", "Latte", "Cappuccino"]
};
// 2. データの内容からETag(ハッシュ値)を生成する
const dataString = JSON.stringify(menuData);
const hash = crypto.createHash('md5').update(dataString).digest('hex');
const etagValue = `"${hash}"`;
// 3. クライアントから送られてきた If-None-Match ヘルパーを確認する
const clientEtag = req.headers['if-none-match'];
// 4. ETagが一致するかどうかを判定
if (clientEtag === etagValue) {
// 中身が変更されていないので 304 を返却(ボディは空)
return res.status(304).end();
}
// 5. 変更がある場合は、ETagヘッダーを添えて通常の200レスポンスを返す
res.setHeader('ETag', etagValue);
res.json(menuData);
});
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
サーバーサイドのフレームワークやCDN(CloudflareやCloudFrontなど)によっては、このあたりのETag比較や304の返却を自動でハンドリングしてくれる機能も備わっています。まずは仕組みの本質を理解し、適切なインフラ構成を選択することがエンジニアとしての腕の見せ所ですね。
—
まとめ
今回は、ETagとIf-None-Matchを用いた条件付きリクエストの仕組みを紐解いてきました。
- データの変更有無を「合言葉(ETag)」で比較する
- 変わっていなければ
304 Not Modifiedを返して通信量を劇的に削減する - ネットワークの帯域とサーバーの負荷を同時に優しくできる
一見すると地味なHTTPの仕組みですが、こうした細やかな最適化の積み重ねが、サクサク動く快適なWeb体験を支えています。ぜひ皆さんの開発するAPIやインフラ設計にも取り入れてみてくださいね!
コメント