先に要点
- CORS エラーの大半は、プリフライト(OPTIONS)が失敗している か レスポンスヘッダが正しく付いていない のどちらか。ブラウザのエラーメッセージは表面的なので、必ず DevTools のネットワークタブで OPTIONS のレスポンスを確認するのが原因特定の出発点。
- 典型的なハマりパターンは 10 個に大別できる: Origin の指定ミス」 「Credentials とワイルドカードの衝突」 「プリフライト未対応」 「許可ヘッダ / メソッド漏れ」 「プロキシ層での重複ヘッダ」 「ブラウザのプリフライトキャッシュ」 「Cookie の SameSite 衝突」 「エラーレスポンスにヘッダが付かない」 「Service Worker / CDN の介在」 `フレームワーク標準と手書きの混在。
- デバッグの王道は ① ブラウザの DevTools で失敗したリクエストを特定 → ② OPTIONS のレスポンスヘッダを確認 → ③ 本リクエストのレスポンスヘッダを確認 → ④ サーバ側ログで再現 → ⑤ プロキシ層を一段ずつ確認。「サーバ側だけ」 「フロント側だけ」 を見ても原因が見えないことが多い。
- 「 CORS を緩めて回避」 は最後の手段。「 Access-Control-Allow-Origin: *」 を本番でつけると認証情報を一切送れない(「Credentials: true」 と両立しない)し、「セキュリティリスク」 も増える。原則は 許可する Origin をホワイトリストで明示。
- 「 自前で CORS ヘッダを書く」 のはほぼ事故のもと。フレームワーク標準のミドルウェア(Laravel の 「HandleCors」、Express の 「cors」 パッケージ、Spring の 「CorsConfiguration」 など) を使い、設定だけで済ませるのが安全。
「CORS で詰まった」 ── Web 開発者なら誰もが通る試練です。CORS の仕組み自体は 「異なる Origin 間のリクエストを安全に行うためのブラウザ機構」 とシンプルですが、「どこでヘッダが正しく付いていないか」 を特定するのが難しく、多くの時間を吸い取ります。
ざっくり言うと、CORS エラーは プリフライト(OPTIONS)が失敗 / レスポンスヘッダが不足 / プロキシ層で改変される のいずれかが原因です。エラーメッセージは表面的なので、「どのリクエストの、どのヘッダが、なぜ来ていないか」 を切り分ける手順を持っているかどうかで、解決速度が大きく変わります。
この記事では、実務で踏みやすい 10 個のハマりパターン と、原因特定のための デバッグチェックリスト を整理します。CORS の基礎は CORS とは?初心者向けの解説 を併読してください。
ハマりパターン 10 と直し方
それぞれの失敗パターンと、原因 + 直し方をセットで整理します。
1. Origin の指定ミス
最も基本的で、最も頻発するパターン。
症状
` https://example.com を許可したつもりが、「https://www.example.com からのリクエストが弾かれる」。「末尾スラッシュの有無」 「スキーム(http/https)の違い」 「ポート番号の違い」 で別 Origin 扱い。
対処
許可リストは スキーム + ホスト + ポートを完全一致で指定。動的に複数許可するなら、「許可リストと突き合わせて該当する Origin だけ Access-Control-Allow-Origin に返す」 設計にする。* (ワイルドカード)を本番で使わない。
2. Credentials とワイルドカードの衝突
仕様の最重要ポイントの一つで、見落とすと永遠に解決しない。
症状
「 Cookie や Authorization ヘッダを送りたいので fetch に credentials: include」 を付けたら、「Access-Control-Allow-Origin: * と credentials は両立できない」 エラー。
3. プリフライト(OPTIONS)未対応
サーバ側で 「OPTIONS メソッドを受け付ける設定が無い」 ケース。
4. 許可ヘッダ / メソッド漏れ
「POST は通るのに PATCH が通らない」 「Content-Type は OK だが Authorization が通らない」 系。
症状
「 Access-Control-Allow-Methods」 や 「Access-Control-Allow-Headers」 に必要な値が含まれていない。`プリフライトの OPTIONS リクエストで、ブラウザが送る 「Access-Control-Request-Headers」 と一致しないと弾かれる」。
対処
サーバ側で 許可するメソッドとヘッダを明示的に設定。「Authorization」 「Content-Type」 「X-Requested-With」 のような追加ヘッダはそれぞれ明示が必要(「* で全許可」 は credentials と両立不可)。
5. プロキシ層での重複ヘッダ
「 ALB / CloudFront / nginx / Cloudflare」 を経由すると起きる難しいパターン。
症状
「 バックエンドアプリも CORS ヘッダを返し、プロキシも CORS ヘッダを足す」 ことで、Access-Control-Allow-Origin が2つ になり 「仕様違反でブラウザがブロック」。
対処
CORS は1箇所だけで処理する。アプリ側で処理するなら、プロキシ側では足さない。CloudFront / API Gateway のような前段で処理するなら、アプリ側のミドルウェアは外す。どこで CORS が付与されているかを必ずネットワークタブで確認。
6. ブラウザのプリフライトキャッシュ
「 設定を直したのにエラーが消えない」 系の典型。
症状
サーバ側で CORS 設定を修正したのに、ブラウザはまだ ` 古い OPTIONS のレスポンスをキャッシュしている。「Access-Control-Max-Age」 で指定した秒数の間、プリフライトを再送しない。
対処
` DevTools の Network タブで 「Disable cache」 にチェック」、または シークレットウィンドウで確認。修正中は 「Access-Control-Max-Age を 60 秒など短く」 設定しておくとデバッグが楽。本番では長め(600〜86400 秒)に。
7. Cookie の SameSite 衝突
「 Cookie が送信されない」 系で、近年 SameSite デフォルト変更で増えた。
8. エラーレスポンスに CORS ヘッダが付かない
「 200 はうまく行くのに 500 だけ CORS エラー」 という不思議な状況。
症状
「 エラー時(500、404)に CORS ミドルウェアを通らない実装」 になっていて、エラーレスポンスに 「Access-Control-Allow-Origin」 が付かない。ブラウザは CORS エラーとして処理し、実際のエラー詳細が見えない。
対処
CORS ミドルウェアを エラーハンドラより前段に置く。フレームワークによっては 「エラーハンドラ後でも CORS を付ける設定」 が必要。「必ず正常系とエラー系の両方でテスト」 する。
9. Service Worker / CDN の介在
「 ローカルでは通るのに本番だけエラー」 系。
症状
「 Service Worker」 がリクエストをインターセプトして CORS ヘッダを落とす、「CloudFront / Cloudflare」 がレスポンスヘッダをキャッシュしたりキャッシュキーで Vary を考慮していない、で起きる。
対処
Service Worker の fetch ハンドラ内で Response の cors モードを明示。CloudFront の キャッシュポリシーに Origin ヘッダを含める 設定。CloudFront の入門 で扱った 「キャッシュキー設計」 と同じ話。
10. フレームワーク標準と手書きの混在
「動かないので CORS ヘッダを自分で書き足した」 が事故るパターン。
症状
` Laravel の HandleCors が動いている上に、ルート内で手書きの header(`Access-Control-...「) を追加」 で、ヘッダが重複したり矛盾する値になる。
デバッグチェックリスト
エラーに遭遇したら、この順序で確認すると原因に最短で到達できます。
「どこで失敗しているか」 を切り分けると、「修正する場所」 が一意に決まります。
CORS を扱う時の設計原則
「目の前のエラーを直す」 だけでなく、「今後ハマらない設計」 を作るための原則を整理します。
CORS は 1 箇所だけで処理
「 プロキシ層・アプリ層・複数ミドルウェア」 のうち 「 どこか 1 箇所」 に統一。「複数で処理するとヘッダ重複」 が起きる。「API Gateway を最前段にして CORS を処理 → バックエンドは CORS を意識しない」 が現代的。
同一 Origin に寄せられるなら寄せる
「 フロントと API が別ドメイン」 だと CORS の悩みが永遠に続く。フロントと API を同一ドメインに統一(リバースプロキシで 「/api/*」 を裏のバックエンドに振る、BFF パターン)。CORS の悩みが構造的に消える。
CORS に関するよくある質問
Q. CORS エラーはサーバ側?フロント側?
A. ほぼ常にサーバ側の設定問題です。CORS はブラウザが安全のためにブロックする仕組みで、「サーバが正しいヘッダを返せばブラウザが許可する」 という流れ。フロント側の修正で消えるのは Origin / Credentials / mode の設定ミスのときくらいで、本質的にはサーバ側を直すことになります。
Q. プリフライト(OPTIONS)はいつ送られますか?
A. 「 シンプルリクエスト」 以外の場合に送られる。シンプルリクエストは 「GET / HEAD / POST(application/x-www-form-urlencoded、multipart/form-data、text/plain のみ)」 で、「カスタムヘッダなし」 が条件。「JSON で POST」 「Authorization ヘッダ付き」 「PUT / DELETE / PATCH」 はプリフライトが発生します。
Q. 「Access-Control-Allow-Origin: *」 を本番で使ってはダメ?
A. 公開 API(認証不要)なら OK、認証が絡むなら NG。「Cookie や Authorization ヘッダ」 を送る場合、ワイルドカードと credentials は両立できないので、「具体的なドメインを返す」 必要があります。「公開データを返す API」(オープンデータ、CDN 配信用 API)では 「*」 が適切な場合もあります。
Q. CloudFront / API Gateway を前段に置いたら CORS はそこで処理?
A. 推奨される現代的な構成です。CloudFront や API Gateway の機能で CORS ヘッダを付与し、バックエンドアプリは CORS を意識しない設計にすると、「バックエンドの差し替えやリプレース」 が楽になります。「CORS は前段の責務」 と決めておくのが運用上シンプル。
Q. Service Worker で CORS が変わる?
A. 変わります(意図的に設定すれば)。Service Worker の fetch ハンドラで 「Response の cors モード」 や 「cache のレスポンス」 を返すと、ブラウザの CORS 判定が変わります。「オフライン対応の SW がレスポンスを操作」 で、「CORS ヘッダが落ちる」 事故が起きやすい。SW 経由の fetch も忘れず確認。
Q. ローカル開発で CORS を回避するベストプラクティスは?
A. フロントの dev サーバで API へのプロキシを設定するのが定番。「Vite / Next.js / CRA」 のいずれも 「proxy 設定」 で 「/api/* を localhost:3001 に転送」 のような設定が可能。同一 Origin として扱われるので CORS が不要になります。「本番でも同一 Origin に寄せる」 ことを目指せば、CORS の悩みは構造的に消えます。
Q. CORS と CSRF はどう違いますか?
A. 守る対象が違う。CORS は 「ブラウザがクロスサイトリクエストを安全にする仕組み」、CSRF は 「攻撃者が他人を装って認証済みリクエストを送らせる攻撃」。「CORS が正しく動いているから CSRF も守られる」 は 必ずしも成立しない。CSRF 対策は別途 CSRF トークン or SameSite Cookie で行う必要があります。
まとめ
CORS エラーは プリフライトが失敗 / レスポンスヘッダ不足 / プロキシ層の改変 のいずれかが原因です。`エラーメッセージは表面的なので、「DevTools の Network タブで OPTIONS の応答を確認する」 のが原因特定の出発点。
「目の前のエラーを直す」 だけでなく、1 箇所だけで CORS を処理 / 同一 Origin に寄せる / ホワイトリスト / フレームワーク標準を使う という設計原則を守れば、CORS の問題は構造的に減ります。「手書きヘッダで何とかする」 のは事故のもとなので、フレームワークの標準ミドルウェアに統一するのが現代の正解です。
参考リンク
- MDN: Cross-Origin Resource Sharing (CORS)
- MDN: CORS errors
- WHATWG: Fetch Standard
- AWS Docs: API Gateway で CORS を有効化
- Web.dev: Cross-Origin Resource Sharing (CORS)