テックブログ

CORSとは?プリフライトリクエストで詰まらないための基本

CORSとは?プリフライトリクエストで詰まらないための基本

CORSは、ブラウザ上のJavaScriptが別オリジンのAPIへアクセスするとき、サーバー側がその通信を許可しているか確認するための仕組みです。許可設定が足りないと、API自体は動いていてもブラウザがレスポンスを受け取らせません。オリジン、プリフライトリクエスト、許可ヘッダー、Cookieあり通信の基本を理解すると、CORSエラーが出たときに「どこを見ればよいか」を落ち着いて切り分けやすくなります。

1. CORSとは何か

1-1. ブラウザが別オリジン通信を制御する仕組み

CORSは、ブラウザが別オリジンへの通信を安全のために制御する仕組みです。APIサーバーそのものの機能というより、ブラウザが「このレスポンスをJavaScriptへ渡してよいか」を判断するためのルールです。

WebページのJavaScriptは、同じオリジンのリソースへアクセスする前提で動いています。もし制限がなければ、悪意あるサイトが別のサービスへ勝手にアクセスして、ユーザーの情報を読み取ろうとする危険があります。そこでブラウザは、別オリジン通信をするとき、サーバーが明示的に許可している場合だけレスポンスを読み取らせるようにしています。

ここで大事なのは、CORSは認証や認可の仕組みではないことです。「ログイン済みか」「このユーザーに権限があるか」は別の話で、CORSはあくまでブラウザの安全制御です。つまり、CORSが通ったから安全というわけでもなく、CORSエラーが出たからサーバーが壊れているとも限りません。

1-2. オリジンとは何か(スキーム・ホスト・ポート)

CORSを理解するうえでまず押さえたいのが、オリジンはスキーム・ホスト・ポートの組み合わせだという点です。パスはオリジンに含まれません。

たとえば、https://example.com:443 というURLなら、https がスキーム、example.com がホスト、443 がポートです。この3つのどれか1つでも違えば、別オリジンになります。つまり、httphttps が違っても別、api.example.comexample.com でも別、30008080 でも別です。

開発中によくあるのは、フロントが http://localhost:3000、APIが http://localhost:8080 のようにポート違いで動いているケースです。このとき「どちらもlocalhostだから同じ」と思いがちですが、実際には別オリジンなのでCORS対象になります。CORSで詰まったら、まずURL全体ではなくスキーム・ホスト・ポートの3点を見比べると整理しやすいです。

1-3. APIが動いていてもブラウザでブロックされる理由

CORSで混乱しやすいのは、API自体は正常に応答していても、ブラウザがそのレスポンスをブロックすることがある点です。ここが「サーバーは動いているのに画面では失敗する」理由です。

たとえば、curlやPostmanではAPIが成功するのに、ブラウザからのfetchではCORSエラーになることがあります。これは、curlやPostmanのようなHTTPクライアントにはブラウザの同一オリジン制約がないからです。つまり、同じHTTP通信に見えても、ブラウザだけが追加の安全確認をしている状態です。

そのため、CORSエラーを見たときは「APIが死んでいる」と決めつけないことが大切です。まずは、サーバーがレスポンスを返しているか、そのレスポンスに必要なCORSヘッダーがあるか、ブラウザのコンソールで何が足りないと言われているかを確認するのが基本です。

2. CORSエラーが起きる典型パターン

2-1. Access-Control-Allow-Origin が不足している

CORSエラーで最も多いのは、Access-Control-Allow-Origin が返っていない、または期待したオリジンが入っていないケースです。これは別オリジン通信を許可する基本ヘッダーなので、ここが足りないとブラウザは止めます。

ブラウザは、リクエスト元のオリジンに対して、サーバーが明示的に許可を返しているかを見ます。たとえばフロントが http://localhost:3000 なら、そのオリジンが許可対象として返ってくる必要があります。ここが空だったり、別のオリジンを返したりすると、ブラウザはレスポンスをJavaScriptへ渡しません。

Access-Control-Allow-Origin: http://localhost:3000

この例では、http://localhost:3000 からのアクセスだけを許可しています。注意点は、何でもかんでも * にすればよいわけではないことです。特にCookieや認証情報を扱う場合は * では動かないうえ、公開範囲を広げすぎる設計にもつながるので、必要なオリジンを明示する考え方が基本です。

2-2. 許可されていないメソッドやヘッダーを使っている

Access-Control-Allow-Origin だけでなく、使っているHTTPメソッドやリクエストヘッダーがサーバー側で許可されていないとCORSエラーになります。特にPOST/PUT/DELETEや独自ヘッダーで詰まりやすいです。

ブラウザは、別オリジン通信で安全確認が必要と判断すると、「このメソッドやヘッダーを使ってよいですか」と事前確認を行います。そのとき、サーバー側が Access-Control-Allow-MethodsAccess-Control-Allow-Headers で適切に返していないと、本体リクエストへ進めません。つまり、オリジン許可だけでなく、使い方まで許可が必要です。

よくあるのは、Authorization ヘッダーや Content-Type: application/json を送っているのに、サーバー側の許可ヘッダー設定が足りないケースです。CORSエラーが出たら、「Originだけ見て終わり」にせず、「MethodとHeadersも合っているか」を必ず見ると切り分けやすくなります。

2-3. Cookieあり通信で設定が足りない

Cookieを含む通信では、通常のCORS設定より追加で注意が必要です。ここを理解していないと、「Originは合っているのに動かない」という状態になりやすいです。

ブラウザでCookieや認証情報を別オリジンへ送るときは、フロント側で credentials を有効にし、サーバー側でも Access-Control-Allow-Credentials: true を返す必要があります。さらに、Cookie自体の SameSite 設定も関係します。つまり、CORSヘッダーだけ整っていても、Cookieの設定が合わないと認証つき通信は成立しません。

Access-Control-Allow-Origin: https://frontend.example.com
Access-Control-Allow-Credentials: true

この例では、特定オリジンからのCookieあり通信を許可しています。注意点は、Access-Control-Allow-Credentials: true を使う場合、Access-Control-Allow-Origin: * は使えないことです。また、Cookie側で SameSite=None; Secure が必要になる場面もあるため、CORSとCookie属性をセットで確認することが重要です。

3. プリフライトリクエストとは

3-1. OPTIONSリクエストが送られる理由

プリフライトリクエストとは、本体リクエストの前にブラウザが送る事前確認用のリクエストです。通常は OPTIONS メソッドで送られます。

ブラウザは、別オリジン通信のうち、安全確認が必要と判断したものについて、いきなり本体を送らずに「このメソッドとヘッダーで送ってよいですか」と先に確認します。これがプリフライトです。サーバーがここで適切に許可を返した場合にだけ、その後の本体リクエストが送られます。

そのため、DevToolsで OPTIONS リクエストが見えたら、「余計な通信が増えた」ではなく「ブラウザが安全確認している」と理解するとよいです。逆に、OPTIONS に対して404や405を返していると、本体まで進めずCORSエラーになります。

3-2. Access-Control-Allow-Methods / Headers の役割

プリフライトでは、サーバーが「どのメソッドとヘッダーなら使ってよいか」を返す必要があります。ここで使うのが Access-Control-Allow-MethodsAccess-Control-Allow-Headers です。

たとえば、ブラウザが「POSTで、Authorizationヘッダー付きで送りたい」と確認したとき、サーバー側が「POSTは許可する」「Authorizationも許可する」と返さなければ、本体リクエストは送られません。つまり、プリフライトは通信可否の交渉のようなものです。

Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization

この例では、GET/POST/OPTIONSと、Content-Type/Authorizationヘッダーを許可しています。注意点は、必要以上に何でも許可しないことです。CORS設定も最小権限の考え方が大切なので、実際に使うMethodとHeaderへ絞るほうが管理しやすく、安全でもあります。

3-3. プリフライトが発生する条件

プリフライトは、すべての別オリジン通信で必ず発生するわけではありません。一定の条件に当てはまるときだけ送られます。

代表的なのは、GET/POST以外のメソッドを使うとき、Content-Type: application/json のような特定の形式を使うとき、Authorizationなどのカスタムヘッダーを付けるときです。逆に、単純なGETや一部の簡単なPOSTではプリフライトなしで本体が送られることもあります。つまり、「CORS通信 = 必ずOPTIONS」ではありません。

初心者が混乱しやすいのは、同じAPIでもリクエスト内容によってプリフライトが出たり出なかったりすることです。たとえば、フォーム送信風のPOSTでは出ないのに、JSON送信に変えたら急にOPTIONSが出ることがあります。CORSで詰まったら、メソッドとヘッダーとContent-Typeの変化を思い出すと原因に近づきやすいです。

4. CORS設定で確認するポイント

4-1. 許可するOriginをどう決めるか

CORS設定でまず決めるべきなのは、どのオリジンからのアクセスを許可するかです。ここは広くしすぎず、必要な相手を明示するのが基本です。

たとえば、開発中は http://localhost:3000、本番では https://app.example.com のように、フロントの実際のオリジンを許可対象にします。もし複数環境があるなら、それぞれを設定へ含めます。何でも受け入れる形にすると、将来の管理が雑になりやすく、Cookieあり通信でも問題が出ます。

実務では、「今どのフロントがこのAPIを使うのか」を一覧化して、それを設定へ反映するのが分かりやすいです。オリジンはURL全文ではなく、スキーム・ホスト・ポートで見ることも忘れないようにすると、localhost開発環境の詰まりを減らせます。

4-2. 許可するMethodとHeaderを必要最小限にする

CORSのMethodとHeaderも、必要最小限に絞るのが基本です。便利だから全部許可する、ではなく、実際に必要なものだけを通す設計が扱いやすいです。

たとえば、読み取り専用APIならGETだけでよいかもしれませんし、認証トークンを送る必要がなければAuthorizationヘッダーを許可する必要はありません。許可範囲を絞ると、設定の意図が分かりやすくなり、問題が起きたときもどこを見直すべきか分かりやすくなります。これはCORSでも最小権限の考え方です。

よくあるのは、最初は何でも通して動かし、そのまま本番へ残してしまうことです。最初に動作確認を広めにやること自体はありますが、最終的には使っているMethodとHeaderへ絞るほうがよいです。設定が増えてきたら、「本当にこのHeaderは必要か」を定期的に見直す価値があります。

4-3. credentialsを使う場合の注意点

Cookieや認証情報を扱うなら、credentials を使う前提でCORSを別物として考える必要があります。ここは通常のCORS設定より注意点が増えます。

フロント側では fetch(..., { credentials: 'include' }) のように送信設定をし、サーバー側では Access-Control-Allow-Credentials: true を返します。さらに、Cookieの SameSiteSecure 属性も正しくないと、Cookie自体が送られません。つまり、「CORS設定」「フロント送信設定」「Cookie属性」の3つがそろって初めて成立します。

fetch('https://api.example.com/profile', {
  method: 'GET',
  credentials: 'include'
});

この例では、Cookieを含めてAPIへアクセスしようとしています。注意点は、CORSヘッダーが正しくても、Cookieの SameSite 設定が厳しすぎると動かないことです。Cookieあり通信で詰まったら、CORSだけでなくCookie属性まで確認する癖を付けると切り分けしやすくなります。

5. デバッグの進め方

5-1. ブラウザのDevToolsで見る場所

CORSエラーを調べるときは、まずブラウザのDevToolsを見るのが基本です。特にConsoleとNetworkタブが重要です。

Consoleには、どのヘッダーが足りないか、どのオリジンが拒否されたか、といったメッセージが出ることがあります。Networkタブでは、OPTIONSリクエストが出ているか、本体リクエストまで進んでいるか、レスポンスヘッダーに何が返っているかを確認できます。つまり、CORSの切り分けはブラウザ上の観察から始めるのが近道です。

初心者はConsoleの赤い文字だけ見て終わりがちですが、Networkタブまで見るとかなり情報が増えます。特に「OPTIONSが失敗しているのか」「本体は返っているのにブラウザが止めているのか」を見分けられると、調査が進みやすくなります。

5-2. サーバーログとネットワークタブの切り分け

CORSエラーでは、ブラウザ側の表示とサーバー側のログを分けて見ることが大切です。両方見ないと、「何が起きたか」を誤解しやすくなります。

たとえば、サーバーログには200が出ているのにブラウザでは失敗している場合、CORSヘッダー不足でブラウザがブロックしている可能性があります。逆に、OPTIONS自体がサーバーへ届いていない、または405で落ちているなら、サーバー設定側の問題かもしれません。つまり、HTTPとしての成功・失敗と、ブラウザでの利用可否は別で考える必要があります。

デバッグ時は、「サーバーは何を返したか」と「ブラウザはそれをどう扱ったか」を分けると整理しやすいです。片方だけ見ていると、「サーバーは成功しているから問題ない」または「ブラウザが怒っているからAPIが死んでいる」といった誤解が起きやすくなります。

5-3. よくある誤解と確認チェックリスト

CORSで詰まったときは、よくある誤解を先に外すだけでも解決しやすくなります。特に初心者は、似た症状を同じ原因だと思いやすいです。

代表的な誤解は、「CORSはサーバーエラーそのもの」「Postmanで通るならブラウザも通る」「Access-Control-Allow-Origin: * で全部解決」「CORSは認証の仕組み」といったものです。実際には、CORSはブラウザの安全制御であり、HTTPクライアント一般の話ではありません。さらに、Cookieあり通信やプリフライトの条件が加わると、単純な1ヘッダーでは解決しないことも多いです。

  • フロントとAPIのオリジンは本当に同じか
  • Access-Control-Allow-Origin は正しい値を返しているか
  • OPTIONSリクエストが失敗していないか
  • Access-Control-Allow-MethodsHeaders が足りているか
  • Cookieあり通信なら Allow-CredentialsSameSite を見たか
  • ブラウザのConsoleとNetworkの両方を見たか

この順で確認すると、かなり多くのCORSエラーを切り分けやすくなります。最初から複雑な設定を疑うより、オリジン、プリフライト、Cookieの3点から順に見るほうが混乱しにくいです。

6. まとめ

6-1. CORSはブラウザ側の安全制御

CORSは、ブラウザが別オリジン通信を安全のために制御する仕組みです。APIサーバーの機能のように見えても、実際にはブラウザが「このレスポンスを使ってよいか」を判断しています。

そのため、Postmanやcurlでは通るのにブラウザでは失敗することがあります。オリジン、プリフライト、許可ヘッダー、Cookieあり通信の条件がそろっているかを見ないと、原因が見えにくくなります。CORSは認証や認可ではなく、ブラウザに特有の安全制御だという整理が重要です。

まずは「別オリジンかどうか」を確認し、次に「OPTIONSが必要か」「許可ヘッダーが足りているか」を見るだけでも、かなり詰まりにくくなります。CORSは難しい設定というより、通信条件を順番に確認する問題だと捉えると理解しやすいです。

6-2. CORSエラー確認チェックリスト

最後に、CORSエラーで最低限確認したいポイントをまとめます。全部を暗記するより、この順番で見ることが大切です。

  • フロントとAPIのスキーム・ホスト・ポートは同じか
  • Access-Control-Allow-Origin は正しいオリジンを返しているか
  • OPTIONSリクエストが送られているか、成功しているか
  • Access-Control-Allow-MethodsAccess-Control-Allow-Headers は足りているか
  • Cookieあり通信なら Access-Control-Allow-CredentialsSameSite を確認したか
  • ConsoleだけでなくNetworkタブも見たか

CORSで大切なのは、「とりあえずヘッダーを増やす」ではなく、ブラウザが何を確認しているかを理解することです。仕組みが分かると、エラー文を見たときに落ち着いて原因を絞りやすくなります。

7. 参考リンク