CORS のプリフライトはいつ発生するか:OPTIONS リクエストが先に送られる条件

fetch() が本来のリクエストの前に OPTIONS を送る条件を WHATWG Fetch 標準で整理しました。セーフリストのメソッドとヘッダー、application/json と Authorization でプリフライトが起きる理由、Max-Age の既定値5秒も扱います。

開発WebHTTP

別オリジンの API に fetch() で Content-Type: application/json の JSON を POST するとき、一致する CORS-preflight キャッシュ項目がなければ、開発者ツールに同じ URL への OPTIONS リクエストがもう1件表示されます。これが CORS のプリフライト(preflight)リクエストです。本来のリクエストを送る前に、サーバーが CORS プロトコルを理解しているかをブラウザーが確かめる手順です。

この記事は WHATWG Fetch 標準(2026年9月21日更新版)と RFC 9110 に基づいています。Fetch は更新が続く Living Standard なので、節番号や細かな手順は変わる可能性があります。以下は標準が定める動作で、個々のブラウザーの実装は確認していません。

プリフライトが付く条件

同一オリジンへのリクエストは CORS の手順を通りません。別オリジンへの fetch() と XMLHttpRequest のリクエストには、標準内部の unsafe-request フラグが立ちます。このようなリクエストは、次のいずれかに当てはまるとプリフライトによる許可が必要です。ただし、一致する CORS-preflight キャッシュ項目があれば、ブラウザーは新しい OPTIONS リクエストを省略できます。

  • メソッドが GET、HEAD、POST ではない。
  • セーフリスト(CORS-safelisted)にないリクエストヘッダーがある。
  • 本文に ReadableStream を使っている、または XMLHttpRequestUpload にイベントリスナーを登録している。

セーフリストに入るリクエストヘッダーは Accept、Accept-Language、Content-Language、Content-Type で、Range は bytes=0-499 のような単一範囲のときだけ対象になります。値が128バイトを超えるとリストから外れます。Content-Type は MIME タイプが application/x-www-form-urlencoded、multipart/form-data、text/plain のいずれかである必要があります。これらのヘッダーの値の長さが合計1024バイトを超えた場合も、すべて安全でないヘッダーとして扱われます。Fetch 標準に「シンプルリクエスト(simple request)」という用語はなく、あるのはこれらの条件だけです。

リクエスト プリフライトが必要 理由
fetch(url) なし GET で追加ヘッダーがない
fetch(url, { method: "POST", body: JSON.stringify(data) }) なし 文字列の本文には Content-Type: text/plain;charset=UTF-8 が付く
上に "Content-Type": "application/json" を追加 あり application/json はセーフリストにない
headers: { Authorization: "Bearer ..." } あり Authorization はセーフリストにない
method: "DELETE" あり セーフリストのメソッドではない

2行目で Content-Type を自分で指定しないと、Request が本文の種類に合った値を補います。そのためサーバーは JSON 文字列を text/plain として受け取ります。

プリフライトがないからといって、CORS の検査がないわけではありません。リクエストはそのままサーバーに届き、レスポンスが CORS チェックを通らなければ、ブラウザーはスクリプトにネットワークエラーを返します。サーバー側ではすでに処理が終わっていることもあります。

リクエストと、通るレスポンス

JSON を Authorization 付きで POST すると、プリフライトは次のようになります(主なヘッダーのみ)。

OPTIONS /api/items HTTP/1.1
Origin: https://app.example
Accept: */*
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization,content-type

Access-Control-Request-Headers には、セーフリスト外のヘッダー名が小文字で並べ替えられ、空白なしのカンマでつながります。

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

通るには、ステータスコードが200〜299で、Access-Control-Allow-Origin がリクエストのオリジンと同じか * である必要があります。Cookie などを一緒に送る credentials モード "include" では * は使えず、Access-Control-Allow-Credentials: true も必要です。このモードではメソッドとヘッダーの * もワイルドカードとして扱われません。Authorization はモードに関係なく、Access-Control-Allow-Headers に名前を明記しなければなりません。チェックに失敗すると、本来のリクエストは送られません。

プリフライトのリクエストには、Cookie や Authorization のような資格情報が含まれません。認証ミドルウェアが OPTIONS にもトークンを求めて401を返すと、ステータスコードの条件にかかってプリフライトは失敗します。

結果のキャッシュ

Access-Control-Max-Age は、許可されたメソッドとヘッダーの情報を何秒キャッシュするかを決めます。ヘッダーがなければ5秒です。ブラウザーは上限を設けることができますが、その値は標準になく、この記事でも確認していません。各キャッシュ項目には、ネットワークパーティションキー、シリアライズされたオリジン、URL、max-age、credentials のフラグ、そしてメソッドまたはヘッダー名が保存されます。max-age を過ぎたら削除しなければならず、それより前に削除されることもあります。

RFC 9110 は OPTIONS へのレスポンスをキャッシュ不可と定めています。プリフライトの結果は HTTP キャッシュではなく、Fetch 標準が別に設けた CORS-preflight キャッシュに残ります。

401 レスポンスそのものの意味は 401と403の違い で扱いました。

参照した仕様

  • WHATWG Fetch Standard(Living Standard、2026年9月21日更新):2.2.1 Methods、2.2.2 Headers、2.2.3 Statuses、2.2.5 Requests、3.3.2–3.3.5 CORS protocol、4.1 Main fetch、4.4 HTTP fetch、4.8 CORS-preflight fetch、4.9 CORS-preflight cache、4.10 CORS check、5.2 BodyInit unions、5.4 Request class
  • RFC 9110 — HTTP Semantics:9.3.7 OPTIONS

リンクと内容は2026年10月5日に確認しました。

ブログへ