CORS のプリフライトはいつ発生するか:OPTIONS リクエストが先に送られる条件
fetch() が本来のリクエストの前に OPTIONS を送る条件を WHATWG Fetch 標準で整理しました。セーフリストのメソッドとヘッダー、application/json と Authorization でプリフライトが起きる理由、Max-Age の既定値5秒も扱います。
別オリジンの 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日に確認しました。