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

ブラウザの「勝手な思い込み」を飼いならす:CORSプリフライトリクエストの全貌と実務的レシピ

ネットワークエンジニアとして数々の現場を渡り歩いてくると、「なぜかAPI通信ができない」「ローカル環境では動くのにステージング環境でCORSエラーが出る」といった相談を、若手エンジニアから本当によく受ける。

ブラウザのコンソールを開き、あの赤字で吐き出される Access-Control-Allow-Origin の文字を見て頭を抱えた経験はないだろうか。

CORS(Cross-Origin Resource Sharing)は、Webのセキュリティの要である「同一オリジンポリシー(Same-Origin Policy)」の厳格な縛りを、意図した安全な範囲で緩めるためのメカニズムだ。しかし、このCORSの裏側で何が起きているのか、RFC(Fetch仕様)レベルのパケットの往復を意識している人は意外と少ない。

今回は、API設計やインフラ運用に携わるエンジニアに向けて、CORSの「プリフライトリクエスト」がネットワーク上でどう舞い踊っているのか、その実体を徹底的に解剖していこう。

—

1. そもそもなぜ「プリフライト」が必要なのか?

Webブラウザは非常に疑り深い。異なるオリジン(スキーム・ドメイン・ポート番号のいずれかが異なる場所)へのリクエストを検知すると、「本当にこのリクエストを投げて大丈夫か?」と、本番のデータを送信する前に事前確認(Preflight)を入れる。これがプリフライトリクエストだ。

通信のフローを文字通り「パケットの旅」として追ってみよう。

プリフライトを伴う通信シーケンス

[Browser (Client)]                  [Reverse Proxy / API Gateway]
       |                                          |
       |--- 1. OPTIONS (Preflight) -------------->|  ※「このリクエスト投げていい?」
       |    (Access-Control-Request-Method等)   |
       |                                          |
       |<-- 2. 200 OK (CORS Headers) -------------|  ※「いいよ、許可するメソッドはこれ」
       |    (Access-Control-Allow-Origin等)       |
       |                                          |
       |--- 3. 本番の POST/PUT リクエスト ------->|  ※ よし、データを送る
       |                                          |
       |<-- 4. 200 OK (Response Data) ------------|  ※ 処理結果を返却
       |                                          |

ここで重要なのは、1と2のステップ(OPTIONSメソッドによる事前確認)がブラウザの裏側で自動的に行われているという点だ。開発者がJavaScriptで fetch() や axios を叩いたとき、条件に合致すると、ブラウザはこの OPTIONS リクエストを勝手に先回りして発射する。

—

2. プリフライトが発生する条件とOPTIONSメソッドの正体

すべてのクロスオリジンリクエストでプリフライトが発生するわけではない。以下の条件をすべて満たす「単純リクエスト(Simple Request)」の場合は、プリフライトはスキップされ、いきなり本番リクエストが飛ぶ。

1. メソッドが GET、HEAD、POST のいずれかであること。
2. ヘッダーが以下のセーフなものしか含まれていないこと:

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type(ただし application/x-www-form-urlencoded, multipart/form-data, text/plain のみ)

3. リクエストに ReadableStream が使用されていないこと。

逆に言うと、近年のモダンなWeb APIで多用される Content-Type: application/json や、カスタムヘッダー(例: Authorization: Bearer <token> や X-Requested-With)を付与した瞬間に、この条件から外れる。結果として、ブラウザは必ず OPTIONS メソッドによるプリフライトリクエストを強制するのだ。

プリフライトでやり取りされる主要なHTTPヘッダー

ブラウザが OPTIONS リクエストを送るとき、サーバーに対して以下の「意向」を伝える。

  • Origin: 自分のオリジン(例: https://app.example.com)
  • Access-Control-Request-Method: これから投げたい本番リクエストのメソッド(例: PUT や DELETE)
  • Access-Control-Request-Headers: これから使いたいカスタムヘッダー(例: Authorization, Content-Type)

これに対し、サーバー側(NginxやAPI Gateway、アプリケーションサーバー)は、正しく設定されていれば以下の応答ヘッダーを返す。

  • Access-Control-Allow-Origin: 許可するオリジン(* または特定のドメイン)
  • Access-Control-Allow-Methods: 許可するHTTPメソッドのリスト
  • Access-Control-Allow-Headers: 許可するヘッダーのリスト
  • Access-Control-Max-Age: このプリフライト結果をブラウザがキャッシュしてよい秒数(無駄な OPTIONS 通信を減らすための命綱)

—

3. 実務でハマる!CORS設定の落とし穴とセキュリティリスク

「とりあえず動かすために Access-Control-Allow-Origin: * にしておけばいいや」
――この設計は、インフラ・セキュリティの観点から見ると大罪に近い。

特に Access-Control-Allow-Credentials: true(CookieやAuthorizationヘッダーなどの認証情報を含めたクロスオリジン通信を許可する設定)を有効にする場合、Access-Control-Allow-Origin にアスタリスク(*)を指定することは仕様上禁止されており、ブラウザはエラーを吐く。ワイルドカードではなく、明確なドメインを動的に、あるいは静的にマッピングする必要がある。

また、Access-Control-Allow-Headers を怠ると、フロントエンド側で認証トークンを付与した瞬間にプリフライトで弾かれるという「よくある事故」が起きる。

—

4. 実践:NginxおよびPython(FastAPI)での正確なCORS設定

では、実務でどのようにインフラやアプリケーションを構築すべきか。具体的な設定例を見ていこう。

パターンA: Nginx(リバースプロキシ)でのプリフライト(OPTIONS)ハンドリング

APIサーバーの手前にNginxを置いている場合、ブラウザから飛んでくる OPTIONS リクエストをアプリケーションに到達させる前にNginx側で即座にハンドリング(204 No Contentを返却)してしまうのが、バックエンドの負荷軽減の観点からも非常にスマートだ。

server {
    listen 80;
    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-Allow-Credentials' 'true' always;
            
            # プリフライト結果を10分間ブラウザにキャッシュさせる(OPTIONSパケットの嵐を防ぐ)
            add_header 'Access-Control-Max-Age' 600 always;
            
            add_header 'Content-Type' 'text/plain charset=UTF-8';
            add_header 'Content-Length' 0;
            return 204;
        }

        # 通常の本番リクエスト時のCORSヘッダー付与
        add_header 'Access-Control-Allow-Origin' 'https://app.example.com' always;
        add_header 'Access-Control-Allow-Credentials' 'true' 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;

        # バックエンドのアプリケーションへプロキシ
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

パターンB: Python (FastAPI) でのCORSミドルウェア設定

モダンなWebフレームワークでは、ミドルウェア層でCORSを綺麗に管理できる。FastAPIの例を挙げよう。

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

app = FastAPI()

# 許可するオリジンのリスト(本番環境ではワイルドカードを避ける)
origins = [
    "https://app.example.com",
    "https://staging.example.com",
    "http://localhost:3000",  # ローカル開発用
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,             # 許可するオリジンの明示的な指定
    allow_credentials=True,            # Cookieや認証情報の送信を許可
    allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],  # 許可するメソッド
    allow_headers=["Authorization", "Content-Type", "X-Requested-With"],  # 許可するヘッダー
    max_age=600,                       # プリフライトのキャッシュ時間(秒)
)

@app.get("/api/v1/resource")
def read_resource():
    return {"status": "success", "message": "CORS configured properly!"}

—

5. 現場のシニアが教える!CORSトラブルシューティングの極意

もし本番環境でCORSエラーに遭遇したら、感覚でコードを書き換える前に、必ず以下の手順でデバッグを行ってほしい。

1. curl で直接 OPTIONS を飛ばしてみる
ブラウザを介さず、CURLの -X OPTIONS とカスタムヘッダーを使って、サーバーが意図したレスポンスヘッダーを返しているかをダイレクトに確認する。

curl -i -X OPTIONS https://api.example.com/api/v1/resource \
     -H "Origin: https://app.example.com" \
     -H "Access-Control-Request-Method: PUT" \
     -H "Access-Control-Request-Headers: Authorization, Content-Type"

ここで Access-Control-Allow-Origin が返ってきていない、あるいはステータスコードが 200 や 204 以外(403 や 500)になっている場合、NginxやAPI Gateway、あるいはアプリケーションのミドルウェア層での設定ミス、もしくは前段のWAF(Web Application Firewall)によるブロックが疑われる。

2. ブラウザの開発者ツール(Networkタブ)で OPTIONS リクエストを探す
赤字のエラーログだけに注目してはいけない。その手前にある OPTIONS リクエストの「レスポンスヘッダー」を凝視するのだ。ブラウザは「サーバーが返したCORSヘッダーの条件」と「自分がやりたいリクエストの条件」を突合してエラーを出している。何が足りないのか(例えば Access-Control-Allow-Headers に指定したヘッダーが含まれていない等)は、すべてそこに書かれている。

3. リバースプロキシのログフォーマットを見直す
Nginxなどのアクセスログに $request_method や $status だけでなく、リクエストヘッダー($http_origin など)を記録できるようにカスタムフォーマットを組んでおくと、外部からの不正なオリジンによるアクセスや、予期せぬプリフライトの挙動が一発で可視化できるようになり、運用保守のフェーズで劇的に助けられる。

—

まとめ

CORSプリフライトリクエストは、一見すると「開発者を悩ませる面倒な仕組み」に見えるかもしれない。しかしその実態は、Webというオープンなネットワーク上で、悪意あるサイトからのCSRFや不正なデータ強奪を防ぐためにブラウザが用意してくれた、極めて堅牢な防壁である。

この仕組みと通信フロー、そして各ヘッダーの持つ意味を正確に理解していれば、どんな複雑なマルチドメイン構成やクラウドアーキテクチャであっても、迷うことなくセキュアで美しいAPIエンドポイントを設計・運用できるようになるはずだ。

明日からのインフラ設計やAPI実装に、ぜひこの知見を役立ててほしい。

コメント

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