先に要点
- CORS エラーの多くは、プリフライト(OPTIONS)が失敗しているか、応答に必要なヘッダーが付いていないかのどちらかです。コンソールのメッセージだけでは原因が分かりにくいので、DevTools の Network タブで OPTIONS と本リクエストの応答ヘッダーを確認するのが出発点です。
- 実務で踏みやすいのは次の 10 パターンです。Origin の指定ミス、認証情報とワイルドカードの衝突、プリフライト未対応、許可するヘッダー・メソッドの漏れ、前段での重複ヘッダー、プリフライトのキャッシュ、Cookie の SameSite、エラー応答のヘッダー漏れ、Service Worker や CDN の介在、標準機能と手書きの混在。
- 切り分けは失敗したリクエストを特定 → OPTIONS の応答ヘッダー → 本リクエストの応答ヘッダー → サーバー側のログ → 前段のプロキシを一段ずつの順で進めます。サーバー側だけ、フロント側だけを見ても原因が見えないことが多いです。
Access-Control-Allow-Origin: *は、Cookie などの認証情報を伴うリクエストでは使えません(ブラウザが応答の読み取りを止めます)。認証が絡む API では、許可する Origin を明示した一覧で管理します。- CORS のヘッダーを手書きで足すと事故のもとです。Laravel の HandleCors、Express の cors パッケージ、Spring の CorsConfiguration など、フレームワーク標準の仕組みを設定で使うのが安全です。
CORS で詰まるのは、Web 開発ではよくあることです。CORS の仕組み自体は「別の Origin へのリクエストを、ブラウザが安全に扱うための仕組み」とシンプルですが、どこでヘッダーが付いていないのかを突き止めるのに時間がかかります。
原因の多くは、プリフライト(OPTIONS)の失敗、応答ヘッダーの不足、前段のプロキシでの改変のどれかです。どのリクエストの、どのヘッダーが、なぜ来ていないかを切り分ける手順を持っているかで、解決までの時間が大きく変わります。
この記事では、実務で踏みやすい 10 個のハマりパターンと、原因を特定するためのデバッグチェックリストを整理します。CORS の基礎は CORS とは?初心者向けの解説 をあわせて読んでください。
ハマりパターン 10 と直し方
それぞれの症状と、対処をセットで整理します。
1. Origin の指定ミス
最も基本的で、最もよく起きるパターンです。
症状
https://example.com を許可したつもりが、https://www.example.com からのリクエストが弾かれる。スキーム(http / https)・ホスト・ポートのどれかが違えば別の Origin です。許可リストに https://example.com/ のように末尾のスラッシュ付きで書くと、ブラウザが送る Origin(スラッシュなし)と一致しません。
対処
許可リストはスキーム + ホスト + ポートを完全一致で書きます。複数を許可するなら、リクエストの Origin を許可リストと照合し、一致したものだけを Access-Control-Allow-Origin に返します。
2. 認証情報とワイルドカードの衝突
仕様上の重要な制約で、知らないと解決しません。
症状
Cookie を送るために fetch に credentials: 'include' を付けたら、サーバーが Access-Control-Allow-Origin: * を返していてエラーになる。
3. プリフライト(OPTIONS)未対応
サーバーが OPTIONS メソッドを受け付けていないケースです。
4. 許可するヘッダー・メソッドの漏れ
「POST は通るのに PATCH が通らない」「Content-Type は通るのに Authorization が通らない」という症状です。
症状
Access-Control-Allow-Methods や Access-Control-Allow-Headers に必要な値が入っていない。ブラウザがプリフライトで送る Access-Control-Request-Headers の内容が許可されていないと、そこで止まります。
対処
許可するメソッドとヘッダーを明示します。Authorization や X-Requested-With のような追加のヘッダーは、それぞれ書く必要があります。認証情報付きのリクエストでは、Allow-Headers や Allow-Methods の * も特別な意味を持たなくなるので、具体的に列挙します。
5. 前段のプロキシでの重複ヘッダー
ALB・CloudFront・nginx・Cloudflare を経由する構成で起きやすいパターンです。
症状
アプリも CORS ヘッダーを返し、前段のプロキシも同じヘッダーを足した結果、Access-Control-Allow-Origin が 2 つ付いてしまい、ブラウザが拒否する。
対処
CORS は 1 か所だけで処理します。アプリで処理するなら前段では足さず、前段で処理するならアプリ側の CORS 機能は外します。どこでヘッダーが付いているかは、Network タブの応答ヘッダーで確かめます。
6. プリフライトのキャッシュ
「設定を直したのにエラーが消えない」の典型です。
対処
DevTools の Network タブで「Disable cache」を有効にするか、シークレットウィンドウで確かめます。修正中は Max-Age を短くしておくと楽です。MDN によると、上限はブラウザごとに決まっていて、Chromium は 2 時間(7200 秒)、Firefox は 24 時間(86400 秒)です。指定がないときの既定は 5 秒です。
7. Cookie の SameSite
「Cookie が送られない」という症状で、CORS の設定が正しくても起きます。
症状
別のサイトからの fetch で Cookie が付かない。Chrome と Edge では、SameSite を指定していない Cookie は Lax として扱われるため、別サイトからの fetch には付きません。Safari と Firefox は既定で第三者 Cookie を制限しています。
対処
別サイトから Cookie を送るなら、Set-Cookie に SameSite=None; Secure を付けます(HTTPS が必須)。それでも、第三者 Cookie を制限するブラウザや設定では届きません。長く運用するなら、フロントと API を同じサイト(同じ登録ドメイン)にそろえる設計の方が安全です。ブラウザごとの違いは SameSite と第三者 Cookie の現状 で整理しています。
8. エラー応答に CORS ヘッダーが付かない
「200 は通るのに、500 のときだけ CORS エラーになる」という状況です。
症状
エラー時(500 や 404)の応答が CORS の処理を通らず、Access-Control-Allow-Origin が付かない。ブラウザはCORS エラーとして扱うので、本当のエラー内容が JavaScript から見えません。
対処
CORS の処理をエラー処理より前段に置き、エラー応答にもヘッダーが付くようにします。正常系とエラー系の両方で確認します。
9. Service Worker や CDN の介在
「手元では通るのに本番だけエラー」の典型です。
症状
Service Worker がリクエストを横取りし、mode: 'no-cors' で取り直した中身を見られない応答(opaque レスポンス)を返している。あるいは CDN が、Origin ごとに違うはずの CORS ヘッダーを 1 つの応答としてキャッシュしている。
対処
Service Worker の fetch ハンドラが元のリクエストをそのまま中継しているかを確認します。CloudFront なら、Origin・Access-Control-Request-Headers・Access-Control-Request-Method をオリジンへ転送する設定(管理ポリシーの CORS-S3Origin や CORS-CustomOrigin など)にして、Origin ごとの応答がキャッシュで混ざらないようにします。CloudFront の入門 で扱ったキャッシュキーの設計と同じ話です。
10. フレームワーク標準と手書きの混在
「動かないので CORS ヘッダーを自分で書き足した」が裏目に出るパターンです。
症状
Laravel の HandleCors が動いているのに、ルートの中で Access-Control-Allow-Origin を手で追加して、ヘッダーが重複したり値が矛盾したりする。
デバッグチェックリスト
エラーに遭遇したら、この順で確認すると原因に早くたどり着けます。
- 11. Network タブで失敗したリクエストを特定
赤くなった行を見つけ、その直前に OPTIONS があるかを確認する。プリフライトが必要なリクエスト(JSON の POST、独自ヘッダー付きなど)で OPTIONS が失敗していれば、まずそこを直す。
- 22. OPTIONS の応答ヘッダーを確認
Access-Control-Allow-Origin・Allow-Methods・Allow-Headers が正しく返っているか。ステータスは 200 か 204 か。Allow-Origin が * なのに認証情報を送っていないか。
- 33. 本リクエストの応答ヘッダーを確認
OPTIONS が通っても、本リクエストの応答に Allow-Origin がなければ止まる。エラー応答(500 や 404)にもヘッダーが付いているかを見る。
- 44. Origin の完全一致を確認
スキーム・ホスト・ポートがリクエストの Origin と一致しているか。http と https、www の有無、許可リスト側の末尾のスラッシュを1つずつ確認する。
- 55. 前段のプロキシを確認
ALB・CloudFront・API Gateway・nginx・Cloudflare が間にあるなら、それぞれがヘッダーを足したり消したりしていないか。アプリと前段の両方で付ける二重設定は避ける。
- 6
- 77. Cookie の SameSite を確認
認証情報を送るのに Cookie が付かないなら、SameSite=None; Secure になっているか、ブラウザが第三者 Cookie を制限していないかを確認する。
- 8
どこで失敗しているかを切り分けると、直す場所が 1 つに決まります。
CORS を扱うときの設計原則
目の前のエラーを直すだけでなく、今後ハマらないための原則を整理します。
CORS は 1 か所で処理
前段のプロキシ・アプリ・複数のミドルウェアのうち、どこか 1 か所にまとめます。API Gateway などの前段でまとめて処理し、アプリは CORS を意識しない構成も選択肢の 1 つです。
同じ Origin に寄せられるなら寄せる
フロントと API を同じドメインにそろえ、リバースプロキシで /api/ 以下を裏の API に振り分ける(BFF の構成など)と、CORS そのものが不要になります。
許可する Origin は一覧で管理
認証が絡む API では * を使わず、本番・ステージング・開発の Origin を環境ごとの設定で管理します。リクエストの Origin を照合して、一致したときだけ返します。
CORS に関するよくある質問
Q. CORS エラーはサーバー側とフロント側のどちらの問題ですか?
A. 多くはサーバー側の設定の問題です。CORS は、サーバーが返すヘッダーを見てブラウザが読み取りを許すかどうかを決める仕組みだからです。フロント側の修正で直るのは、呼び出し先の URL や credentials、mode の指定を誤っているときくらいです。
Q. プリフライト(OPTIONS)はいつ送られますか?
A. 「単純リクエスト」の条件を満たさないときに送られます。MDN によると、単純リクエストはメソッドが GET・HEAD・POST のいずれかで、手で付けるヘッダーが Accept や Content-Type などの一部に限られ、Content-Type が application/x-www-form-urlencoded・multipart/form-data・text/plain のいずれかであるものです。JSON の POST、Authorization ヘッダー付き、PUT・DELETE・PATCH ではプリフライトが発生します。
Q. Access-Control-Allow-Origin: * を本番で使ってはいけませんか?
A. 認証の要らない公開データなら使えますが、認証が絡むなら使えません。Cookie などの認証情報を伴うリクエストでは、ワイルドカードを返すとブラウザが応答の読み取りを止めます。公開データを配る API では * が適切な場合もあります。
Q. CloudFront や API Gateway を前段に置いたら、CORS はそこで処理すべきですか?
A. 選択肢の 1 つです。CloudFront や API Gateway でヘッダーを付ければ、アプリを差し替えても CORS の設定が変わりません。どちらで処理するにしても、1 か所にまとめることが大切です。
Q. Service Worker が CORS に影響することはありますか?
A. あります。Service Worker の fetch ハンドラがリクエストを作り直したり、キャッシュした応答を返したりすると、ページから見える応答が変わります。no-cors で取り直した opaque レスポンスは、中身を JavaScript から読めません。オフライン対応を入れたあとに CORS エラーが出始めたら、Service Worker 経由の fetch も確認します。
Q. ローカル開発で CORS を避ける方法はありますか?
A. 開発サーバーのプロキシ機能で、API へのリクエストを転送するのが定番です。Vite なら server.proxy、Next.js なら next.config の rewrites で、/api/ 以下を手元の API サーバーに転送できます。ブラウザから見ると同じ Origin になるので、CORS が要りません。本番でも同じ Origin にそろえられれば、CORS の悩みはなくなります。
Q. CORS と CSRF はどう違いますか?
A. 守る対象が違います。CORS は、別の Origin からの応答を読ませるかどうかをブラウザが決める仕組みです。CSRF は、利用者のブラウザに意図しない認証済みリクエストを送らせる攻撃です。CORS を正しく設定しても、フォーム送信のような単純リクエストは送られてしまうため、CSRF 対策は CSRF トークンや SameSite Cookie で別に行います。
まとめ
CORS エラーの原因は、プリフライトの失敗、応答ヘッダーの不足、前段のプロキシでの改変のどれかであることがほとんどです。コンソールのメッセージだけで判断せず、Network タブで OPTIONS と本リクエストの応答ヘッダーを確かめるのが出発点です。
そのうえで、CORS は 1 か所で処理する、同じ Origin に寄せる、許可する Origin を一覧で管理する、フレームワーク標準を使うという原則を守れば、CORS の問題は大きく減ります。
参考リンク
- MDN: Cross-Origin Resource Sharing (CORS)
- MDN: Set-Cookie(SameSite の既定値)
- MDN: Access-Control-Max-Age
- MDN: CORS errors
- WHATWG: Fetch Standard
- Laravel: Routing - CORS
- AWS Docs: CloudFront のリクエストヘッダーに基づくキャッシュ
- AWS Docs: API Gateway で CORS を有効化
- web.dev: Cross-Origin Resource Sharing (CORS)