【実務・中級編】 Varyヘッダーによるキャッシュキーの制御 – Web APIアーキテクチャ・データ連携実践ガイド

なぜ「キャッシュの毒」が回るのか? Vary ヘッダーで制御するWeb APIの最適化

インフラエンジニアとして現場に立っていると、若手から「CDNやキャッシュサーバーを入れた途端に、特定のユーザーに別の人のデータが見えてしまった」という悲鳴のような相談を受けることがあります。これは、Web API開発において最も初歩的かつ、最も破壊的な事故の一つです。

この事故の多くは、HTTPキャッシュの仕組みを深く理解していないことに起因します。今回は、REST APIのアーキテクチャを支える重要な隠し味、Vary ヘッダーによるキャッシュキーの制御について、現場の知見を交えて徹底解説します。

—

1. キャッシュの仕組みと Vary の役割

Web APIにおいて、キャッシュはレスポンス速度を劇的に向上させる魔法ですが、その対象は「URL」だけではありません。同じURLでも、クライアントが求める形式や権限によってレスポンスの中身は変わります。

デフォルトでは、多くのキャッシュサーバーは「URL(リクエストURI)」のみをキャッシュキー(インデックス)として保持します。しかし、実際には以下のようなヘッダーの違いによって、サーバーは異なるレスポンスを返すべきです。

  • Accept-Encoding: gzipで送るか、br(Brotli)で送るか。
  • Authorization: 認証済みユーザー専用のレスポンスか。
  • Accept-Language: 言語設定は英語か日本語か。

ここで登場するのが Vary ヘッダーです。サーバー側で Vary: Accept-Encoding, Authorization と指定することで、キャッシュサーバーに対し「このURLだけでなく、これらのヘッダーの値もキャッシュキーに含めてくれ」と指示を出せるのです。

—

2. 実務で遭遇する「事故」と通信フロー

例えば、Authorization を Vary に含めず、ユーザーAの認証情報でキャッシュを作ってしまうとどうなるでしょうか。

1. ユーザーAがリクエストを送る。サーバーは Authorization を見て個別のデータを返す。CDNはこのレスポンスを「URLのみ」をキーとしてキャッシュする。
2. ユーザーBが同じURLにアクセスする。CDNはキャッシュを探し、ユーザーAのレスポンスがヒットする。
3. ユーザーBに、なぜかユーザーAの機密情報が返される。

これが「キャッシュ汚染(Cache Poisoning)」の正体です。これを防ぐための基本的なシーケンスは以下の通りです。

クライアント -> (リクエスト: Authorization: Token-A) -> CDN -> オリジンサーバー
                                                         |
                                                 [オリジン: Vary: Authorization]
                                                         |
クライアント <- (レスポンス: ユーザーAのデータ) <- CDN (キャッシュ保存: URL + Token-A)

—

3. 実践:Varyヘッダーの適切な設定

Nginxでの設定例

Nginxをリバースプロキシとして使う場合、gzip のような圧縮設定を有効にすると、デフォルトで Vary: Accept-Encoding が付与されます。しかし、API側で独自のヘッダーを制御したい場合は、明示的な設定が必要です。

# /etc/nginx/conf.d/api.conf

location /api/ {
    # レスポンスにVaryを追加する
    # 認証トークンや言語設定に応じてキャッシュを分ける設定
    add_header Vary "Authorization, Accept-Language";
    
    proxy_pass http://backend_api;
}

Python (FastAPI) での実装例

FastAPIのようなモダンなフレームワークでも、レスポンスオブジェクトに対してヘッダーを付与します。

from fastapi import FastAPI, Response

app = FastAPI()

@app.get("/items/{item_id}")
async def get_item(item_id: str, response: Response):
    # 特定のヘッダーに基づいてキャッシュを分けるよう指示
    response.headers["Vary"] = "Authorization, Accept-Language"
    return {"item": "data", "id": item_id}

—

4. 運用上の注意点(ここが重要)

Vary ヘッダーを使いこなす上で、シニアエンジニアとして必ず伝えておきたい「罠」があります。

1. Vary: * の悪夢

Vary: * を指定すると、「すべてのヘッダーが異なる場合はキャッシュを共有しない」という挙動になります。これを使うと、実質的にそのリソースのキャッシュは無効化されます。* はデバッグ時に一時的に使うことはあっても、本番環境で多用するのはパフォーマンスを著しく低下させるため避けましょう。

2. キャッシュの断片化(キャッシュヒット率の低下)

Vary に指定するヘッダーの種類が多すぎると、キャッシュのバリエーションが爆発します。例えば、User-Agent を Vary に含めると、ユーザーのブラウザが変わるたびに別のキャッシュが生成され、CDNのキャッシュヒット率がゼロに近い状態になります。「本当に必要なヘッダーだけ」に絞るのが鉄則です。

3. デバッグの方法

開発中に正しく設定されているかを確認するには、curl コマンドでヘッダーを覗くのが一番です。

# ヘッダーのみを表示して確認
curl -I -H "Authorization: Bearer my-token" https://api.example.com/data

出力結果に Vary: Authorization が含まれていれば、キャッシュサーバーに正しく情報が伝わっています。

—

最後に:ネットワークは「行間」を読む

API設計において、プロトコルの仕様書であるRFCを読むことは重要ですが、実際にパケットを流してみると「仕様書には書いていない挙動」や「インフラ機器特有の癖」に出会います。

Vary ヘッダーは、クライアントとサーバーの間の「情報の解釈のズレ」を埋めるための重要な橋渡しです。このヘッダーを適切に管理することは、単なるパフォーマンスチューニングではなく、「ユーザーの情報を守る」というセキュリティ意識の表れでもあります。

皆さんのAPIが、より堅牢で、かつ高速なレスポンスを返せるよう、ぜひ今日からヘッダーの構成を見直してみてください。何かハマったことがあれば、またいつでも相談してください。現場からは以上です。

コメント

タイトルとURLをコピーしました