はじめに
CORS でつまずく原因の 8 割はこれです:
- 「なぜいきなり OPTIONS が飛ぶの?」
- 「GET なのに CORS エラー?」
- 「curl では通るのに、ブラウザだけ死ぬ」
答えはシンプルで、ブラウザはクロスオリジン通信を 2種類 に分けています。
- Simple Request(事前確認なし)
- Preflight Request(事前確認=OPTIONS あり)
この記事ではこの2つを、ルール・判定・実例・デバッグ視点で“完全整理”します。
大前提:CORS は「ブラウザの読み取り制御」
- サーバーがレスポンスを返しても
- ブラウザが “その中身をJSに渡さない” ことがある
つまり CORS エラーは「通信失敗」ではなく “読み取り拒否” です。
1. Simple Request とは
Simple Request = ブラウザが危険度低いと判断し、事前確認なしで本リクエストを送るもの
Simple Request の条件(全部満たす必要あり)
HTTPメソッドが次のいずれか
GETPOSTHEAD
“手動で付けるリクエストヘッダ” が制限内(代表例)
AcceptAccept-LanguageContent-Language-
Content-Type(ただし次の3つのいずれかに限る)application/x-www-form-urlencodedmultipart/form-datatext/plain
“特殊なヘッダ” を付けていない
例:Authorization, X-API-Key, X-Requested-With などを 付けた瞬間にアウト(=Preflight)
2. Preflight とは
Preflight =「このリクエスト、送っていい?」を先に OPTIONS で確認する仕組み
ブラウザが先にこう聞きます:
「
https://client.exampleからPUTで、Authorization付きで送るけど、いい?」
Preflight が発生する代表条件(どれか1つでも該当)
- メソッドが
PUT / PATCH / DELETEなど(GET/POST/HEAD以外) -
Content-Type: application/json(←これ超多い) -
AuthorizationやX-*などのカスタムヘッダ -
fetch()でcredentials: "include"を使う場合も設計次第で要注意
3. 実例で“発動条件”を体に染み込ませる
例A:Simple(GET)
fetch("https://api.example.com/users");
GET で余計なヘッダも無し
→ Preflightなし
例B:Preflight(POST + JSON)
fetch("https://api.example.com/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Alice" })
});
Content-Type: application/json がアウト
→ Preflight発生
例C:Preflight(Authorization)
fetch("https://api.example.com/me", {
headers: { "Authorization": "Bearer xxx" }
});
Authorization はシンプルヘッダじゃない
→ Preflight発生
例D:Preflight(PUT)
fetch("https://api.example.com/users/1", {
method: "PUT",
headers: { "Content-Type": "text/plain" },
body: "hello"
});
PUT は Simple の対象外
→ Preflight発生
4. Preflight の実際の通信フロー
ブラウザ内部ではこうなります。
ポイントはここ:
- OPTIONS が 通らない と、本リクエストは 送られない
- OPTIONS が通っても、本リクエストのレスポンスに CORS ヘッダが無いと 読めない
5. サーバーが返すべきヘッダ(最小セット)
Simple Request を読ませたいだけなら
レスポンスにこれが必要:
Access-Control-Allow-Origin: https://client.example
Preflight を通したいなら(OPTIONS のレスポンスに)
Access-Control-Allow-Origin: https://client.example
Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 600
そして 本リクエストのレスポンスにも Access-Control-Allow-Origin は必要です。
(ここ忘れて「OPTIONS は通るのに CORS エラー」になる人、多い)
6. “Simple なのに CORS エラー”の典型パターン
パターン1:サーバーは 200 を返してるのに JS が読めない
→ Access-Control-Allow-Origin が無い/合ってない
パターン2:リダイレクトが挟まる
- 301/302 先のレスポンスに CORS ヘッダが無い
- あるいは途中で Origin が変わる
パターン3:CDN / Proxy がヘッダを落としてる
- 返してるつもりでも、途中で消される
7. “即判断”チェックリスト
次のどれかがあれば Preflight確定:
Content-Type: application/json-
Authorizationを付ける -
X-...のヘッダを付ける - メソッドが
PUT / PATCH / DELETE - ブラウザが「なんか怖い」と感じる構成(リダイレクト多、特殊ヘッダ等)
逆に Simple はだいたいこれ:
- GET / HEAD
- POST でも
text/plainorform-urlencodedで余計なヘッダなし
8. よくある地雷:Allow-Origin と Credentials の組み合わせ
覚え方はこれ:
Cookie を跨いで送る(credentials)なら、Allow-Origin は
\*にできない
つまりこれは 禁止:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
まとめ
- Simple Request:条件が厳しい代わりに、事前確認なし
- Preflight:条件を外れると OPTIONS で許可を取りに行く
- 実務で一番多い Preflight の原因は
application/jsonAuthorization