ゆるテックノート

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