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

CORSプリフライトの深層:なぜその「見えないOPTIONS」はAPIを遅くするのか?

ネットワークエンジニアとして数々の現場を渡り歩いてくると、アプリケーションエンジニアから「本番環境で突然APIが叩けなくなった」「フロントエンドからPOSTできない」という悲鳴交じりの相談をよく受ける。原因をパケットキャプチャやブラウザの開発者ツールで追っていくと、決まって犯人として現れるのが CORS(Cross-Origin Resource Sharing) と、その裏でこっそりやり取りされている プリフライトリクエスト(Preflight Request) だ。

「APIサーバーのコードは完璧なのに、なぜかブラウザが弾く」
「Access-Control-Allow-Origin を設定したはずなのに動かない」

この手のトラブルは、HTTPの仕様やブラウザのセキュリティモデルを表面的な理解だけで済ませていると、泥沼にハマりやすい。今回は、RFC 7231やFetch Living Standardの世界に踏み込み、ブラウザとAPIサーバーの間で何が起きているのかを、パケットの挙動からインフラ側の設定、コードレベルの対策まで徹底的に紐解いていこう。

—

1. CORSプリフライトとは何か? RFCが定めた「安全確認」の儀式

そもそも、なぜCORSが必要なのか。現代のWebブラウザは、同一オリジンポリシー(Same-Origin Policy)という厳格なセキュリティ境界のなかにいる。https://example.com のJavaScriptから、異なるオリジンである https://api.example.com へ勝手にリクエストを送り、レスポンスを盗み見られてはセキュリティの崩壊を意味する。

しかし、モダンなWebアプリケーションでは、フロントエンド(SPA)とバックエンド(API)が別ドメインや別ポートで稼働していることは日常茶飯事だ。ここで「正当な理由があるクロスオリジン通信」を安全に許可する仕組みとして定義されたのがCORSである。

そして、その通信の安全性を事前に確認するための「偵察部隊」こそが、OPTIONS メソッドを使ったプリフライトリクエストなのだ。

プリフライトが発動する条件

ブラウザは、いきなり本番のリクエスト(POSTやPUTなど)を投げない。以下の条件のいずれかに当てはまる場合、ブラウザは本体のリクエストの前に、自動的に OPTIONS メソッドのプリフライトリクエストを送信する。

1. メソッドが GET、POST、HEAD 以外(例: PUT、DELETE、PATCH など)である。
2. リクエストヘッダーに、CORSセーフリストヘッダー(Accept、Accept-Language、Content-Language、Content-Type など)以外のカスタムヘッダー(例: Authorization や X-Requested-With など)が含まれている。
3. Content-Type の値が application/x-www-form-urlencoded、multipart/form-data、text/plain のいずれでもない(例: application/json など)。

つまり、現代のモダンなWeb API(JSONを送り、Bearerトークン認証を行う構成)において、プリフライトリクエストを避けることは実質的に不可能である。すべての非トリビアルなリクエストの裏で、必ずこの事前確認が行われているのだ。

—

2. 通信フローの全貌:ブラウザとサーバーの裏側の会話

実際の通信がどのように行われているのか、シーケンスを見てみよう。ここでは、JavaScriptの fetch() を使って、別オリジンのAPIサーバーへ application/json のデータを POST 送信するシナリオを考える。

[Browser (Client)]                  [API Server / Reverse Proxy]
       │                                         │
       │── 1. OPTIONS (Preflight) ──────────────>│  ※「これからPOSTしていい?」
       │   (Origin, Access-Control-Request-Method)│
       │                                         │
       │<── 2. 200 OK / 204 No Content ──────────│  ※「いいよ、以下の条件でね」
       │   (Access-Control-Allow-Origin, etc.)   │
       │                                         │
       │── 3. POST (Actual Request) ────────────>│  ※「じゃあデータを送るよ」
       │   (Origin, Content-Type: application/json)│
       │                                         │
       │<── 4. 200 OK (Actual Response) ─────────│  ※「処理完了、データこれね」
       │   (Access-Control-Allow-Origin)         │

このように、1回のリクエストを飛ばすために、実質2回のHTTP往復(Round Trip)が発生している。これが、CORSプリフライトがAPIのレイテンシに悪影響を与えると言われる所以だ。

実際のパケット(HTTPヘッダー)の挙動を覗く

開発者ツールのネットワークタブや、curl でこの挙動をシミュレートしてみよう。

ステップ1:ブラウザが自動送信するプリフライトリクエスト

OPTIONS /api/v1/users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization, content-type
  • Origin: どのオリジンからリクエストしているかを告げる。
  • Access-Control-Request-Method: この後に送りたい本体のメソッド(POST)を宣言する。
  • Access-Control-Request-Headers: 本体のリクエストで使用したいカスタムヘッダーを宣言する。

ステップ2:APIサーバーからのレスポンス(許可の証)

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: POST, OPTIONS
Access-Control-Allow-Headers: authorization, content-type
Access-Control-Max-Age: 86400
  • Access-Control-Allow-Origin: 許可するオリジン(ワイルドカード * も可能だが、認証情報を伴う場合は注意が必要)。
  • Access-Control-Allow-Methods: 許可するHTTPメソッドの一覧。
  • Access-Control-Allow-Headers: 許可するヘッダーの一覧。ここにしれっと authorization を書き忘れると、認証付きAPIは沈黙する。
  • Access-Control-Max-Age: このプリフライト結果をブラウザがキャッシュしてよい秒数(例: 86400 = 24時間)。これがないと、APIを叩くたびに毎回 OPTIONS が飛んでネットワークが爆発する。

—

3. 実務で役立つ!Nginx・Apache・各種言語での設定レシピ

インフラエンジニアとして現場に出ると、「CORSの設定を入れてくれ」という依頼がアプリケーションチームから飛んでくる。リバースプロキシ(NginxやApache)で一括制御するか、アプリケーション層でハンドリングするかはアーキテクチャの選択次第だが、双方の設定例を記しておく。

パターンA: Nginxによるリバースプロキシでの一括制御

APIサーバーの手前にNginxを置いている場合、Nginx側で OPTIONS メソッドをインタセプトし、即座に204レスポンスを返すのが最もパフォーマンスが高く、アプリケーションの負荷を軽減できる。

server {
    listen 443 ssl;
    server_name api.example.com;

    location /api/ {
        # プリフライトリクエスト(OPTIONS)に対する直接応答
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' 'https://app.example.com' always;
            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
            add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Requested-With' always;
            add_header 'Access-Control-Max-Age' 86400 always; # 24時間キャッシュ
            add_header 'Content-Type' 'text/plain charset=UTF-8';
            add_header 'Content-Length' 0;
            return 204;
        }

        # 通常のバックエンドへのプロキシ設定
        proxy_pass http://backend_upstream;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;

        # 通常リクエスト用のCORSヘッダー付与
        add_header 'Access-Control-Allow-Origin' 'https://app.example.com' always;
        add_header 'Access-Control-Allow-Credentials' 'true' always; # Cookie等を使う場合は必須
    }
}

パターンB: Python (FastAPI) によるアプリケーション層での制御

モダンなPythonフレームワークであるFastAPIでは、ミドルウェアを使って数行でCORSを有効化できる。

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

# CORSミドルウェアの定義
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://app.example.com"],  # 許可するオリジンのリスト("*"は本番では非推奨)
    allow_credentials=True,                     # CookieやAuthorizationヘッダーの送信を許可
    allow_methods=["*"],                        # すべてのメソッドを許可(OPTIONS含む)
    allow_headers=["*"],                        # すべてのヘッダーを許可
)

@app.post("/api/v1/users")
def create_user():
    return {"message": "User created successfully"}

—

4. トラブルシューティング:現場でよくある「ハマりどころ」とデバッグ手法

CORSやプリフライトに関するトラブルシューティングにおいて、シニアエンジニアが真っ先に確認するポイントをいくつか共有しよう。

1. Access-Control-Allow-Origin にワイルドカード * と Access-Control-Allow-Credentials: true の同時指定はできない

セキュリティ上の理由から、認証情報(CookieやHTTP Basic認証、Bearerトークンなど)を伴うリクエストにおいて、許可オリジンを * にすることはブラウザ仕様で禁止されている。

  • 対策: 許可するオリジンを明示的なドメイン(例: https://app.example.com)に指定するか、リクエストの Origin ヘッダーを動的に読み取ってそのままレスポンスの Access-Control-Allow-Origin に反射(リフレクション)させる実装にする。

2. リバースプロキシが OPTIONS メソッドをバックエンドにスルーしてエラーになる

バックエンドのフレームワークが OPTIONS メソッドに対応していない場合、405 Method Not Allowed が返り、ブラウザは「CORSエラーだ」と勘違いして処理を止めてしまう。

  • 対策: NginxやAPI Gatewayなどのレイヤーで OPTIONS リクエストをトラップし、バックエンドに到達する前に 204 No Content を返すようにインフラ側でルーティングを最適化する。

3. デバッグのための curl コマンド活用法

ブラウザを通さずに、直接サーバーのCORS挙動をテストするには、curl の -X OPTIONS とカスタムヘッダーを使ったプロトコルレベルの確認が最も確実である。

curl -i -X OPTIONS https://api.example.com/api/v1/users \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: authorization, content-type"

このコマンドを叩いた際に、意図した通り Access-Control-Allow-Origin や Access-Control-Allow-Methods が返ってきているかを自分の目で確認する。インフラ・ネットワークを扱う人間にとって、ブラウザのコンソールを開く前にこの curl でHTTPステータスとヘッダーを直視する習慣こそが、トラブルシューティングを最速で終わらせる最大の武器となる。

—

まとめ

CORSプリフライトリクエストは、一見すると「開発者を悩ませる足かせ」のように思えるかもしれない。しかし、オープンなインターネットの荒野において、悪意あるスクリプトから機密性の高いAPIを守るためにブラウザが用意してくれた、極めて合理的で堅牢な防壁である。

プロトコルの仕様(なぜOPTIONSが飛ぶのか、どのヘッダーが必要なのか)を正しく理解し、Nginxなどのインフラ層とアプリケーション層の役割分担を明確に設計すること。それさえできれば、CORSはもはや「得体の知れないエラー」ではなく、手なずけるべき確実なコントロール下にある技術となるはずだ。

コメント

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