はじめに
「Access-Control-Allow-Origin がどうこう…」というあのエラー、何度見ても胃が痛くなりませんか。
CORS(Cross-Origin Resource Sharing)は、Web 開発を始めたほぼ全員が一度はぶつかる壁です。ですが、仕組みさえ理解してしまえば、対処は驚くほど単純です。
本記事では、CORS を「ビルの受付」にたとえて、その目的・動作・解決方法まで、世界一やさしい言葉で解説します。
対象読者
- Web 開発を始めて間もない方
-
fetchやaxiosで API を叩いたら CORS エラーが出て困っている方 - 「とりあえずプロキシで回避」してきたけど、ちゃんと仕組みを理解したい方
この記事でわかること
- CORS が「そもそも何のために存在するのか」
- ブラウザの裏側で起きている 2 種類のリクエスト (シンプル / プリフライト)
- エラーメッセージの読み方
- 開発・本番で使える 正しい解決方法
TL;DR
- CORS は 「他人のサイト(オリジン)から勝手にあなたのサイトの API を叩かれないようにする」 ブラウザのセキュリティ機能。
- ブロックしているのは ブラウザ。サーバーは「OK だよ」というお墨付き(HTTP ヘッダー)を返すだけ。
- 解決の本筋は 「API サーバー側で適切な
Access-Control-Allow-*ヘッダーを返す」 こと。 - 開発中の応急処置として プロキシ(Vite / Next.js / webpack-dev-server) が使える。
- フロントエンド側でヘッダーを書き換えてもエラーは消えない(よくある勘違い)。
そもそも「オリジン」とは
CORS を語る前に、まず オリジン(Origin) という用語を押さえます。
オリジンは次の 3 点セット のことです。
スキーム :// ホスト : ポート
https :// example.com : 443
この 3 つのうち 1 つでも違えば「別オリジン」 扱いになります。
| URL 1 | URL 2 | 同一オリジン? |
|---|---|---|
https://example.com/a |
https://example.com/b |
⭕ 同じ |
https://example.com |
http://example.com |
❌ スキームが違う |
https://example.com |
https://api.example.com |
❌ ホストが違う |
https://example.com:443 |
https://example.com:8080 |
❌ ポートが違う |
パス(/a、/b の部分)はオリジン判定に 関係しません。
なぜ CORS が必要なのか ―「ビルの受付」のたとえ
ここからが本題です。CORS のキモは Same-Origin Policy(同一オリジンポリシー) という、ブラウザの基本ルールにあります。
たとえ話:あなたは A 社のビルにいる
- あなた(ブラウザ)は A 社(
https://a.com)のオフィスにいます。 - A 社のオフィスから、A 社の 社内資料サーバー(
https://a.com/api)には自由にアクセスできます。 - しかしオフィスのパソコンを使って、B 社(
https://b.com/api)の社内サーバーに勝手に問い合わせるのは…おかしいですよね。
このように、「自分がいるオリジンとは違うオリジンへのアクセス」を、ブラウザは原則ブロック します。これが Same-Origin Policy です。
なぜブロックする必要があるのか
たとえばあなたが 銀行サイトにログイン中 だとします。同じブラウザの別タブで悪意のあるサイトを開いたとき、もし制限がなければ、その悪意あるサイトの JavaScript が 裏であなたの銀行 API を勝手に叩けてしまう からです。
Same-Origin Policy が守っているのは「ユーザーのログインセッション」と「Cookie」です。これがなければ、Web は今のように成立していません。
CORS は「例外的に許可するための仕組み」
とはいえ、現実には 正当な理由で別オリジンの API を叩きたい ことが山ほどあります(例:https://my-app.com から https://api.my-app.com を呼ぶ)。
そこで登場するのが CORS です。CORS は、「サーバーが OK を出したオリジンに限り、アクセスを許可する」 という、Same-Origin Policy のための公式な抜け道です。
CORS = Same-Origin Policy を 緩めるための仕組み
ここを誤解すると一生迷子になります。CORS は「制限」ではなく 「許可するための仕組み」 です。
受付のやりとり ― ブラウザとサーバーの会話
ビルの受付にたとえて、ブラウザとサーバーの実際のやりとりを見てみます。
シンプルリクエスト(顔パスで通す)
GET や、ヘッダーが最小限の POST などは シンプルリクエスト と呼ばれます。受付にたとえると 「顔パスで一旦通すけど、後でハンコのチェックはする」 イメージです。
ポイントは 「ブラウザはリクエスト自体は送る」 ことです。サーバーが許可ヘッダー(Access-Control-Allow-Origin)を返さなければ、ブラウザは レスポンスを JS に渡さずに破棄 します。
ここがハマりポイント。サーバーのログには 正常リクエストとして記録される のに、フロントには CORS エラーしか見えません。「サーバーまでは届いている」という事実を知っておくとデバッグが楽になります。
プリフライトリクエスト(先に予約確認する)
PUT・DELETE や、Content-Type: application/json を使う POST、独自ヘッダー(Authorization 以外のカスタムヘッダーなど)を含む場合、ブラウザは 本リクエストの前に、許可を取りに行く 動作をします。これが プリフライト(preflight) です。
受付にたとえると 「いきなり訪問する前に、電話で『〇〇社の▲▲ですが、明日 10 時にお伺いしてもいいですか?』と先に予約確認する」 ような動作です。
プリフライトの返答に必要なヘッダーが揃っていないと、ブラウザは本リクエストを送りません。
| ヘッダー | 意味 |
|---|---|
Access-Control-Allow-Origin |
どのオリジンを許可するか(* か、特定のオリジン) |
Access-Control-Allow-Methods |
許可する HTTP メソッド(例:GET, POST, PUT, DELETE) |
Access-Control-Allow-Headers |
許可するリクエストヘッダー(例:Content-Type, Authorization) |
Access-Control-Max-Age |
プリフライト結果のキャッシュ秒数(再問い合わせを減らせる) |
Cookie を送るとき ― credentials
ログインセッションなど Cookie を一緒に送りたい場合は、もう一段だけルールが厳しくなります。
| 必要なもの | 値 |
|---|---|
| クライアント側 |
fetch(url, { credentials: 'include' }) / axios なら withCredentials: true
|
| サーバー側 ① | Access-Control-Allow-Credentials: true |
| サーバー側 ② |
Access-Control-Allow-Origin に * は使えない。具体的なオリジンを返す。 |
Access-Control-Allow-Origin: * と Access-Control-Allow-Credentials: true の 組み合わせは禁止 です。ブラウザはエラーにします。Cookie を扱うときは必ず オリジンを 1 つに絞って 返してください。
エラーメッセージの読み方
代表的なエラーを 3 つだけ。ここを押さえれば 8 割は読めます。
① No 'Access-Control-Allow-Origin' header is present
Access to fetch at 'https://api.example.com/data' from origin 'https://app.example.com'
has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
→ サーバーが許可ヘッダーを返していない。サーバー側に CORS 設定を入れる必要があります。
② The 'Access-Control-Allow-Origin' header has a value '...' that is not equal to the supplied origin
→ 返しているオリジンが 要求元と一致していない。https://app.example.com を呼んでいるのにサーバーが https://www.example.com を返している、などです。
③ Response to preflight request doesn't pass access control check
→ プリフライトで弾かれている。多くの場合、OPTIONS メソッドのレスポンスが Access-Control-Allow-Methods や Access-Control-Allow-Headers を返していません。サーバー側のフレームワーク設定を確認しましょう。
正しい解決方法
解決方法 1:サーバー側に CORS 設定を入れる(本筋)
これが 唯一の正攻法 です。代表的な実装例を挙げます。
import express from 'express';
import cors from 'cors';
const app = express();
app.use(cors({
origin: 'https://app.example.com', // 許可するオリジン
credentials: true, // Cookie を扱うなら true
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.example.com"],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Content-Type", "Authorization"],
)
Access-Control-Allow-Origin: * は便利ですが、認証付き API には絶対に使わない でください。誰でもどこからでも叩ける状態になります。許可するオリジンは必要最小限に絞り込みましょう。
解決方法 2:開発中はプロキシで回避(応急処置)
ローカル開発で「CORS のためだけにサーバーをいじりたくない」場合、フロント側の開発サーバーで プロキシ を立てると、ブラウザから見た送信先がフロントと 同一オリジン になり、CORS が発生しなくなります。
import { defineConfig } from 'vite';
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
});
フロントからは fetch('/api/users') のように 相対パス で叩けば、開発サーバーが裏で https://api.example.com/users に転送してくれます。
プロキシは 「開発中の便利機能」 であって、CORS の根本解決ではありません。本番では別のドメイン構成になるので、最終的にはサーバー側の CORS 設定が必要です。
解決方法 3:BFF / リバースプロキシ(本番向け)
本番で「フロントと API のドメインを分けたくない」場合、Next.js の Route Handlers や Nginx のリバースプロキシで 同一オリジンに統一する 手があります。これは CORS そのものを発生させない構成です。
サーバー間の通信には Same-Origin Policy が 適用されない ので、自由にやりとりできます。
よくある勘違いトップ 5
CORS のドキュメントを読む前に、まずこれを潰しておくと迷子になりません。
勘違い 1:「CORS はサーバーがブロックしている」
→ ブロックしているのはブラウザです。サーバーは「許可するよ」と書かれたヘッダーを返すだけで、判断するのはブラウザ側。実は curl や Postman には CORS は関係ありません。
勘違い 2:「フロントの fetch にヘッダーを付ければ直る」
→ 直りません。Access-Control-Allow-Origin は レスポンスヘッダー であってリクエストヘッダーではありません。フロントからは付けようがないので、headers をいくらいじっても無意味です。
勘違い 3:「mode: 'no-cors' を付ければ解決」
→ 解決しません。no-cors モードはレスポンスを 「不透明(opaque)」 にするだけで、JS からはレスポンス本文を読めなくなります。「エラーは消えるけど何も取れない」という最悪の状態になります。
// ❌ よくある間違い
const res = await fetch('https://api.example.com/data', { mode: 'no-cors' });
const data = await res.json(); // 何も取れない(opaque レスポンス)
勘違い 4:「同じドメインだから同一オリジンのはず」
→ ポート番号が違うと別オリジン です。http://localhost:3000 と http://localhost:8080 は 別オリジン 扱いになります。
勘違い 5:「ブラウザ拡張で CORS を無効化すればいい」
→ 自分の開発機ではエラーが消えますが、ユーザーの環境では発生し続けます。根本対策にはなりません。一時的な切り分け用途に留めましょう。
デバッグの定石
CORS エラーに当たったら、次の順で切り分けると速いです。
- ブラウザの DevTools → Network タブを開く
- 失敗したリクエストを選び、
OPTIONS(プリフライト)が出ているか確認する -
レスポンスヘッダーに
Access-Control-Allow-*が含まれているか を見る - 含まれていなければ サーバー側の設定不足、含まれているけど値が違うなら オリジンや許可ヘッダーの不一致
- それでも分からなければ
curl -v -H "Origin: https://app.example.com" -X OPTIONS https://api.example.com/dataで生レスポンスを確認
curl でリクエストして 200 が返るのに、ブラウザでだけ CORS エラーが出る場合、それは ブラウザが正しく仕事をしている証拠 です。CORS の知識が間違っているわけではありません。
まとめ
CORS の本質は、たった 3 つです。
- CORS は「許可するための仕組み」(制限する仕組みではない)
- 判断するのはブラウザ、サーバーは許可ヘッダーを返すだけ
- 正しい解決はサーバー側の
Access-Control-Allow-*の設定
ここさえ分かっていれば、初見のエラーも「ああ、サーバーがあのヘッダーを返してないんだな」と落ち着いて対処できます。次に CORS エラーに遭遇したときは、ぜひ DevTools の Network タブ を開いて、ブラウザとサーバーの会話を眺めてみてください。
参考資料