CORSプレフライトとOPTIONSの挙動
フロントでPOSTを叩いたら突然OPTIONSが飛んできた——それがCORSプレフライトです。いつ発生するのか、どう返せばよいのかを簡潔にまとめました。
いつ発生する?
ここでは別オリジンへのfetchなど、CORSが適用される要求を扱います。同一オリジンへのJSON POSTだからといってCORSプレフライトが発生するわけではありません。必要な許可が専用キャッシュにあればOPTIONSを省略できます。
トリガー条件(代表例)
- メソッドがGET/HEAD/POST以外(PUT/PATCH/DELETEなど)。
- Content-Typeがapplication/jsonなど、CORSの許可リストにない場合。許可リストのメディアタイプはapplication/x-www-form-urlencoded、multipart/form-data、text/plainですが、値の長さや文字にも条件があります。
- スクリプトがAuthorizationやX-…など許可リスト外の要求ヘッダーを指定する場合。ほかにもストリームの要求本文など、Fetchで定めるトリガーがあります。
ブラウザが送るヘッダー
プレフライトは実リクエストの許可条件を問い合わせます。Cookieや実リクエスト用のAuthorizationは付けず、送信予定のメソッドと必要なヘッダー名を伝えます。実処理の認証・権限確認は実リクエスト側で行います。
主なヘッダー
| ヘッダー | 意味 |
|---|---|
| Origin | 通常は要求元のスキーム・ホスト・ポートを表す値。既定ポートは省略され、状況によってnullとなる。 |
| Access-Control-Request-Method | 実リクエストで送りたいメソッド。 |
| Access-Control-Request-Headers | 許可確認が必要な要求ヘッダー名の一覧。値ではなく小文字の名前をカンマ区切りで送る。 |
サーバーの返し方
成功には2xxと適切なCORSヘッダーが必要です。204なら本文なし、200でも本文はプレフライト判定には使われません。通常のAllowヘッダーはAccess-Control-Allow-Methodsの代わりになりません。
必須・主なヘッダー
| ヘッダー | 役割 |
|---|---|
| Access-Control-Allow-Origin | 許可する単一Originまたは*。実リクエストのcredentialsがincludeなら*は不可。送信元を許可リストで判定し、無条件に反射しない。 |
| Access-Control-Allow-Methods | 許可するメソッド。大文字・小文字を区別する。PUTなど許可リスト外のメソッドを明示する。 |
| Access-Control-Allow-Headers | 許可する要求ヘッダー名。名前の大文字・小文字は区別しない。Fetch仕様ではAuthorizationは*に含まれないため、実装差に依存せず明示する。 |
| Access-Control-Max-Age | 専用プレフライトキャッシュの秒数。通常のHTTPキャッシュとは別。未指定時の既定値は5秒で、ブラウザーの上限や早期破棄もある。 |
| Access-Control-Allow-Credentials | 実リクエストのcredentialsがincludeなら小文字のtrueが必要。明示したAuthorizationヘッダーの許可とは別で、後者にはAllow-Headersを使う。 |
レスポンスコード
- 204 No Contentで返すのがシンプル。200でも可だがボディは不要。
- 許可しない場合は403などで拒否できます。CORSが通っても、実リクエストへの認証・権限検査が不要になるわけではありません。
JSON送信の例
https://app.example から https://api.example/items へ、credentials: "omit"、JSON本文とAuthorizationヘッダー付きのPOSTを送る想定です。以下はプレフライトの関連ヘッダーを抜き出した例で、認証情報の値は送っていません。
プレフライト要求
OPTIONS /items HTTP/1.1
Host: api.example
Origin: https://app.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization,content-type
許可応答
要求側は小文字、応答側は大文字を含みますが、ヘッダー名の照合は成功します。この例ではcredentialsがomitなのでAllow-Credentialsは不要です。後続のPOSTで認証を確認し、その応答にもAllow-Originを返します。
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 600
よくある落とし穴
OPTIONSの結果と、実リクエストの応答を分けて確認すると原因を絞れます。
チェックリスト
- Access-Control-Allow-Headersの照合は大文字・小文字を区別しません。許可する必要がある名前を過不足なく確認します。
- credentials: "include"では具体的なAllow-OriginとAllow-Credentials: trueが必要です。Allow-Methods/Allow-Headersの*にもワイルドカードの意味はありません。CookieにはSameSiteやブラウザーの制限も別途適用されます。
- プレフライトにログインCookieを要求すると失敗します。OPTIONSには許可方針を返し、実処理の認証は実リクエストに適用します。Originはcurlなどから任意に送れるため、認証には使えません。
- 実リクエストの応答にも適切なAllow-Originと、必要ならAllow-Credentialsが要ります。Originごとに実応答を変えてキャッシュする場合はVary: Originも検討します。
まとめ
プレフライト不要の要求もあるため、CORSはCSRF対策やサーバー認証の代わりにはなりません。メソッドの性質と通常のHTTPキャッシュも合わせて確認しましょう。
仕様・関連資料
Fetch: CORS protocol / Fetch: CORS-preflight fetch / Fetch: CORS-preflight cache / RFC 9110 §9.3.7