【テクニカル・上級編】 CORS(Cross-Origin Resource Sharing)のプリフライトリクエスト – Web APIアーキテクチャ・データ連携実践ガイド

CORSプリフライトの深淵:OPTIONSメソッドとRTTの「見えないコスト」をハックする

ネットワークの深淵を覗くエンジニア諸君、こんにちは。今日はWeb APIの「作法」として語られることが多い CORS(Cross-Origin Resource Sharing) について、教科書的な説明は一切抜きにして、パケットの挙動とインフラの最適化という観点から解剖していこうと思う。

多くの開発者は Access-Control-Allow-Origin をサーバー側で設定して「動いた」と安心するが、インフラアーキテクトの視点では、その裏で発生している OPTIONS メソッドのプリフライトリクエストこそが、パフォーマンスとセキュリティのボトルネックであることを知っているはずだ。

1. プリフライトリクエストの「残酷な現実」

ブラウザが「安全ではない」と判断したリクエスト(例えば Content-Type: application/json を含む POST や、カスタムヘッダーを付与したリクエスト)を送る際、ブラウザは本丸のデータ通信の前に OPTIONS メソッドによる「身分証明」を要求する。

ここで発生する致命的なオーバーヘッドが RTT(Round Trip Time)の倍増 だ。
TLSハンドシェイクが完了したセッション上であっても、プリフライトが成功するまでは実際のアプリケーションデータは流れない。遅延に敏感なモバイル環境や、クライアントとAPIサーバーの地理的距離が離れている場合、この往復は無視できないレイテンシとしてユーザー体験(UX)を損なう。

2. トランスポート層とRTT削減の戦術

この「無駄な往復」をいかに殺すか。ここがインフラ屋の腕の見せ所だ。

A. Access-Control-Max-Age の極大化

最も単純だが強力なのが、Access-Control-Max-Age ヘッダーの活用だ。これを可能な限り大きく設定することで、ブラウザ側のキャッシュにプリフライトの結果を保持させる。

# Nginx設定例:プリフライトのキャッシュ時間を24時間に延長
location /api/ {
    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' 'https://frontend.example.com';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type';
        # 86400秒(24時間)キャッシュし、RTTを削減する
        add_header 'Access-Control-Max-Age' 86400;
        add_header 'Content-Type' 'text/plain charset=UTF-8';
        add_header 'Content-Length' 0;
        return 204;
    }
}

B. TLS 1.3と0-RTT(ゼロアールティーティー)の活用

CORSのプリフライト以前の問題として、TLSハンドシェイクのコストを削減する必要がある。TLS 1.3であればハンドシェイクは1往復に短縮されるが、クライアントが過去に接続したことがあるサーバーであれば、0-RTT データを利用することで、ハンドシェイクの完了を待たずにアプリケーションデータを送り込むことが可能になる。ただし、0-RTT はリプレイ攻撃のリスクがあるため、冪等(Idempotent)なリクエストに限定するなどの設計上の配慮が不可欠だ。

3. セキュリティ設定:ワイルドカードの誘惑を断つ

時折見かける Access-Control-Allow-Origin: * という設定は、セキュリティの観点からは「家の鍵を全開にして家出する」に等しい。

特に Access-Control-Allow-Credentials: true と組み合わせる場合、ワイルドカードは仕様上拒絶される。認証が必要なAPIであれば、必ずホワイトリストによる動的なドメイン判定を行うべきだ。

# Python (FastAPI) でのセキュアな実装例
from fastapi.middleware.cors import CORSMiddleware

# 許可されたオリジンリスト
origins = [
    "https://app.production.com",
    "https://admin.production.com",
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins, # ワイルドカードを避ける
    allow_credentials=True,
    allow_methods=["GET", "POST", "OPTIONS"],
    allow_headers=["Authorization", "X-Custom-Header"],
)

4. ネットワークバッファとヘッダー圧縮

大規模なAPIトラフィックを捌く際、TCP ウィンドウサイズや TCP_NODELAY (Nagleアルゴリズムの無効化)のチューニングは必須だ。特にプリフライトのような小さなパケットを頻繁にやり取りする場合、Nagleアルゴリズムが有効だと、ACK待ちによる不必要な遅延が発生する。

また、HTTP/2やHTTP/3 (QUIC) を採用している場合、HPACK や QPACK によるヘッダー圧縮が効くため、毎回送られる Authorization トークンや User-Agent などの肥大化したヘッダーも、2回目以降は極小のサイズに圧縮される。

結論:プロトコルの挙動を制御下に置く

CORSのプリフライトは、単なるWeb APIの制約ではない。それはネットワークの物理的限界(光の速度とRTT)と、セキュリティポリシーの境界線上で踊るパケットの呼吸だ。

インフラアーキテクトとして、単にライブラリの Middleware を適用するのではなく、パケットがどこで止まり、どのヘッダーがどれだけのRTTを消費しているかを可視化すること。そして、Access-Control-Max-Age やTLSの最適化を通じて、その通信を「見えないもの」にすることこそが、真のパフォーマンスチューニングであると私は考える。

ネットワークは嘘をつかない。君が書いた一行の設定が、数ミリ秒の遅延を生み、あるいは消し去るのだ。次のデプロイでは、ぜひ tcpdump や Wireshark で、その「プリフライトの影」を追跡してみてほしい。そこには、教科書には載っていないプロトコルの真実が記されているはずだ。

コメント

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