はじめに
フロントエンドや API を触っていると、必ず一度は見るこのエラー。
Blocked by CORS policy
- サーバーは 200 OK
- curl / Postman では普通に動く
- でもブラウザだけ失敗する
これは バグでも設定漏れでもなく、CORSが正しく動いている結果 です。
CORS とは何か
CORS はサーバーの防御機構ではない。
ブラウザが「レスポンスを読んでよいか」を判断するルールである。
ここを誤解すると、CORS は永久に分かりません。
| 観点 | 実際 |
|---|---|
| サーバー | どんな Origin からもリクエストは届く |
| ブラウザ | JS にレスポンスを渡すかを判断 |
| curl / Postman | CORS を完全に無視 |
CORS は通信を止めない
読み取りだけを制御する
なぜ CORS が必要なのか
ブラウザには Same-Origin Policy(同一オリジンポリシー) があります。
-
https://a.comの JS は -
https://b.comのレスポンスを - 勝手に読めない
これがなければ、
- ログイン中の銀行 API
- 社内システム
- 個人情報 API
を 悪意あるサイトから盗み見放題 になります。
しかし現代 Web は、
- SPA(React / Vue など)
- API 分離構成
- フロントとバックが別ドメイン
完全禁止では開発できない
そこで生まれた仕組みが
CORS = 条件付き越境許可
CORS = 条件付き越境許可
CORS は次のような仕組みです。
「このオリジンから来た JS なら、
同一オリジンポリシーを一時的に緩めてよい」
とサーバーが宣言する仕組み
その宣言が、HTTP レスポンスヘッダです。
Access-Control-Allow-Origin: https://frontend.example.com
なぜ 200 OK でも JS から読めないのか
GET /user
Origin: https://frontend.example.com
HTTP/1.1 200 OK
{"name":"Alice"}
- 通信: 成功
- レスポンス: 返っている
それでも:
JS からは読めない
理由
- SOP は常に有効
- CORS ヘッダが無い
- → SOP の例外が与えられていない
つまり:
読めないのは SOP が通常運転しているだけ
CORS種類
仕様上、CORS は 2 種類しかありません。
| 種類 | 内容 |
|---|---|
| シンプルリクエスト | 事前確認(OPTIONS)なし |
| プリフライト付き | 事前確認(OPTIONS)あり |
1. シンプルリクエスト
危険性が低いと判断され、
ブラウザが「先に聞かずに」送るリクエストです。
条件
- メソッド
GET / POST / HEAD - ヘッダ
限定的(Authorization 不可) - Content-Type
text/plainapplication/x-www-form-urlencodedmultipart/form-data
重要
- 送信は無条件
- 読み取りは許可制
2. プリフライト(OPTIONS)
先に聞く =
本リクエストの前に、ブラウザがサーバーへ
「これ送っていい?」と確認すること
その確認に使われるのが OPTIONS メソッドです。
OPTIONS は「ブラウザだけ」が送る
- curl → 送らない(自分で送らない限り)
- Postman → 送らない
- ブラウザ → 勝手に送る
だから:
Postman では動くのに、ブラウザでは CORS エラー
が起きる。
なぜ「先に聞く」必要があるのか
ブラウザの立場で考えてみてください。
「この JS、
・PUT で
・Authorization ヘッダ付きで
・JSON を送ろうとしているもし勝手に送ってダメだったら危ないな…」
だから本番の前に“許可確認”をする
これが プリフライト(Preflight)。
CORSヘッダ
CORSヘッダはすべて「ブラウザ向け」
サーバーがブラウザに出す“読み取り許可証”
- フロントエンドが設定するもの ❌
- API クライアント(curl 等)が見るもの ❌
- ブラウザだけが解釈する ✅
CORSヘッダは2種類に分かれる。
① レスポンス側 CORS ヘッダ(最重要)
Access-Control-Allow-Origin
Access-Control-Allow-Origin: https://frontend.example.com
役割
「この Origin の JS なら読んでいい」
重要ルール
-
*は Cookie 使用時 ❌ - 動的に返す場合は
Vary: Origin必須
Access-Control-Allow-Credentials
Access-Control-Allow-Credentials: true
役割
「Cookie / 認証情報を含めて読んでいい」
注意
Allow-Origin: *
Allow-Credentials: true
ブラウザが強制拒否
Access-Control-Allow-Methods
Access-Control-Allow-Methods: GET, POST, PUT
役割
「どの HTTP メソッドが許可されているか」
- 主に プリフライトの返答
- シンプルリクエストでは参照されないことも多い
Access-Control-Allow-Headers
Access-Control-Allow-Headers: Authorization, Content-Type
役割
「どのリクエストヘッダを使っていいか」
-
Authorizationを使うなら必須 - 大文字小文字は区別されない
Access-Control-Expose-Headers
Access-Control-Expose-Headers: X-Total-Count
役割
「JS から読めるレスポンスヘッダを追加で許可」
デフォルトで読めるのはこれだけ
Cache-Control
Content-Type
Expires
Last-Modified
Pragma
Access-Control-Max-Age
Access-Control-Max-Age: 600
役割
「プリフライト結果を何秒キャッシュしていいか」
- OPTIONS の回数削減
- パフォーマンス改善に重要
② リクエスト側 CORS ヘッダ(ブラウザ専用)
※ 開発者が直接書くことはほぼない
Origin
Origin: https://frontend.example.com
役割
「私はどこから来た JS です」
- ブラウザが自動付与
- curl では自動では付かない
Access-Control-Request-Method
Access-Control-Request-Method: PUT
役割
「これからこのメソッドを使いたい」
- OPTIONS 専用
Access-Control-Request-Headers
Access-Control-Request-Headers: Authorization
役割
「これらのヘッダを使いたい」
- OPTIONS 専用
ヘッダ対応関係(ここ超重要)
| ブラウザの質問 | サーバーの回答 |
|---|---|
| Origin | Allow-Origin |
| Request-Method | Allow-Methods |
| Request-Headers | Allow-Headers |
| Cookie使う? | Allow-Credentials |
質問と回答が噛み合わないと CORS エラー
最小構成
Cookieなし API
Allow-Origin: https://frontend.example.com
Allow-Methods: GET, POST
Allow-Headers: Content-Type
Cookieあり API
Allow-Origin: https://frontend.example.com
Allow-Credentials: true
まとめ
CORS は門番ではない
図書館の司書である
- サーバーは本を渡す
- ブラウザが「読んでいいか」を判断
- 守っているのはサーバーではなくユーザー
この視点を持つと、
CORS エラーは「意味不明な壁」ではなくなります。