CORSエラーの原因と正しい直し方 - preflight・credentials まで整理
最終更新日: 2026年4月19日
Access to fetch at '...' from origin '...' has been blocked by CORS policyというエラーに当たったとき、クライアント側のfetchオプションをいじっても解決しないのが普通です。CORSはブラウザが安全のため課す「同一生成元ポリシー」の例外ルールで、 許可の判定は サーバが返すレスポンスヘッダで行います。 この記事ではCORSの仕組み、よくあるエラーの読み方、Nginx・Expressでの正しい設定、 やってはいけない対処を整理します。
When a blocked by CORS policy error shows up, tweaking fetchoptions on the client will not fix it. CORS is enforced by the browser based onserver-side response headers. This guide walks through how to read the error, the simple-vs-preflight boundary, correct Express and Nginx configs, credentials pitfalls, and common anti-patterns.
図1: 単純リクエストとpreflightの違い
1. エラーメッセージの読み方
主要なエラーパターンと、どこを直すべきかの対応表です。
| エラーの一部 | 意味 | 直す場所 |
|---|---|---|
No 'Access-Control-Allow-Origin' | ヘッダ自体がない | サーバ |
has been blocked by CORS policy: Response to preflight | OPTIONSが200/204を返していない | サーバ / プロキシ |
The value of the 'Access-Control-Allow-Credentials' header in the response is '' | credentials:include だが許可ヘッダが無い | サーバ |
Request header field X-... is not allowed by Access-Control-Allow-Headers | カスタムヘッダが許可されていない | サーバ |
Cannot use wildcard ... when credentials mode is 'include' | credentials時は * NG | サーバ |
ほぼ全ての原因は サーバ側のレスポンスヘッダ不足です。 クライアントで mode:'no-cors' を付けても、レスポンスが不透明 (opaque) になって 中身を読めなくなるだけで、根本解決にはなりません。
2. 単純リクエストとpreflightの境界
ブラウザは「副作用が少なく昔からあった形のリクエスト」だけを単純リクエストとして 即送信し、それ以外は OPTIONS で事前確認します。単純リクエストの条件は:
- メソッドが
GET/HEAD/POSTのいずれか - 手動追加ヘッダが
Accept/Accept-Language/Content-Language/Content-Type(制限あり) のみ Content-Typeがapplication/x-www-form-urlencoded/multipart/form-data/text/plain
application/json や Authorization: Bearer を付けた時点でほぼ全てのAPI呼び出しがpreflightを伴うことになります。 なので「GETなら通るがPOST/PUTで落ちる」というときは、まず OPTIONS の応答を ブラウザDevTools → Network で確認します。
3. 正しいサーバ設定(Express)
// Express + cors パッケージ(推奨)
const cors = require("cors");
app.use(cors({
origin: (origin, cb) => {
const allow = ["https://app.example.com", "https://admin.example.com"];
// origin が undefined の場合 = curl等 → 許可したくなければ false
if (!origin || allow.includes(origin)) return cb(null, true);
return cb(new Error("Not allowed by CORS"));
},
credentials: true, // Cookie/Authorization を送る場合
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization", "X-Request-Id"],
maxAge: 86400, // preflight結果のキャッシュ(秒)
}));ここでハマりやすいのが credentials: true のときは origin: "*" が使えないという点です。CORS仕様上、Cookieを送るなら必ず 個別ドメインを明示する必要があります。
4. 正しいサーバ設定(Nginx)
# Nginx での例(API GWや静的配信前段)
location /api/ {
# 単純リクエスト+preflight 両対応
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin "$http_origin" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization, X-Request-Id" always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Access-Control-Max-Age 86400 always;
return 204;
}
add_header Access-Control-Allow-Origin "$http_origin" always;
add_header Access-Control-Allow-Credentials "true" always;
proxy_pass http://backend;
}add_header は常にレスポンスに必要なので always を忘れずに。 502/504等のエラー応答でヘッダが消えると、そのエラー画面もCORSでブロックされて 真因が分からなくなります。
5. credentials (Cookie) を使うときの3つの条件
- クライアント側
fetch(url, { credentials: 'include' })を指定 - サーバ
Access-Control-Allow-Credentials: true - サーバ
Access-Control-Allow-Origin: <明示ドメイン>(ワイルドカード不可)
SameSite属性の影響で、さらにCookieには SameSite=None; Secureが必要です(Chrome 80以降)。サブドメイン間共有なら SameSite=Lax で十分ですが、 完全なクロスサイトでは None; Secure を使い、HTTPS必須です。
6. よくある誤った対処
- クライアントで
mode:'no-cors'を付ける — レスポンスが opaque になり中身を読めなくなるだけ。根治にはならない。 - ブラウザ拡張でCORSを無効化して開発を進める — 本番ビルドで必ず再燃する。プロキシか正しいサーバ設定で対処する。
- サーバで
Allow-Origin: *を常に返す— Cookie を使うAPIでは動かない。credentials方針と噛み合うか先に決める。 - Next.jsの
rewritesだけで解決しようとする — ブラウザから見て同一生成元にしてしまう手法は強力だが、 外部サービス (Stripe / Auth0 等) には使えない。
7. 開発中のワークアラウンド
本番のサーバ設定が直せない場合の一時しのぎ:
- Next.jsの
next.config.jsでasync rewrites()を設定し、/api/:path* → https://backend.example.com/:path*に中継する - Vite の
server.proxyで同様に中継する - Cloudflare Workers / API Gateway 側でCORSヘッダを強制注入する
いずれも 本番でバックエンドチームと正しい設定を詰めるまでの暫定手段 と割り切ること。
8. よくある質問
Q. なぜブラウザだけCORSがあり、curlでは無いのか?
CORSはブラウザのJavaScriptが勝手に他オリジンのリソースを読み取れないよう守る仕組みです。curlやサーバ同士の通信は、そもそもユーザのCookieやセッションを流用できないので 同じ脅威モデルがありません。
Q. プリフライトが毎回飛んでAPIが遅い
Access-Control-Max-Age を大きめ (86400 = 24時間) に設定すると、 同じエンドポイント・メソッド・ヘッダの組合せについてブラウザがキャッシュします。
Q. 社内ツールは全部 * で問題ないのでは?
Cookie や Authorization を使わず、公開情報だけ返すAPIなら * でも実害は小さい ですが、内部APIが 他のイントラネットサイトから読み取れる状態になります。 基本はオリジンホワイトリスト方式を推奨します。
English summary
A CORS policy error is emitted by the browser, not the server. The server must return the right headers on the actual response and the preflight (OPTIONS) response. Remember three rules: (1) any request withapplication/json or Authorization triggers preflight; (2) when using credentials: "include", Allow-Origin must be an explicit origin (not *) and Allow-Credentials: true is required; (3) in Nginx, always use the always flag on add_header so error responses (502/504) still carry the CORS headers — otherwise the real error is masked.