CORS プリフライトで 400 / 403 が返る原因と対処
「本番だけ OPTIONS が 400 / 403 で落ちて API が叩けない」は CORS で最も頻出する 事故です。原因は OPTIONS ルーティング欠落、認証ミドルウェアが OPTIONS をブロック、必要ヘッダ不足の三択に絞り込めます。
Preflight OPTIONS requests failing with 400/403 almost always come down to one of three causes: missing OPTIONS handler, auth middleware blocking preflight, or missingAccess-Control-Allow-* headers.
TL;DR
OPTIONSは認証・CSRFトークンを要求せず 2xx を返すのが原則Access-Control-Allow-Origin/-Methods/-Headersの3点セットcredentials: "include"を使うなら Origin は*不可
1. プリフライトが飛ぶ条件 / When preflight fires
- メソッドが
GET/HEAD/POST以外 Content-Typeがapplication/jsonなど非simpleAuthorization,X-*などカスタムヘッダ付き
2. 原因別の直し方 / Cause table
| 症状 / Symptom | 原因 / Cause | 対処 / Fix |
|---|---|---|
| OPTIONS 400 | ルーティング未定義 | Express: app.options('*', cors()) 等で明示 |
| OPTIONS 401/403 | 認証ミドルウェアが先に走る | OPTIONS を認証より前でバイパス |
| Missing Allow-Headers | Authorization をサーバが知らない | Access-Control-Allow-Headers: Authorization, Content-Type |
| credentials エラー | Origin: * と credentials を併用 | 具体オリジン + Allow-Credentials: true |
| Chrome: blocked by CORB | JSONに Content-Type が無い | 必ず application/json を返す |
3. サーバ別の最小設定 / Minimum server config
Express (Node.js)
import cors from "cors";
app.use(cors({
origin: "https://app.example.com",
credentials: true,
methods: ["GET", "POST", "PUT", "DELETE"],
allowedHeaders: ["Authorization", "Content-Type"],
}));Nginx
location /api/ {
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin "https://app.example.com" always;
add_header Access-Control-Allow-Methods "GET,POST,PUT,DELETE,OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization,Content-Type" always;
add_header Access-Control-Max-Age 86400 always;
return 204;
}
...
}4. DevTools での切り分け / DevTools triage
- Network タブで失敗中の OPTIONS を選択
- Request Headers に
Access-Control-Request-Method/-Headersがあるか確認 - Response Headers にそれぞれに対応する
Access-Control-Allow-*が返っているか - 200/204 で返っているのに後続 POST が落ちるなら、POST レスポンスの Allow-Origin 不足
5. English summary
A failing CORS preflight (400/403) is usually a server-side issue, not a browser one. Check three things in order: (1) the server has an OPTIONS route, (2) auth middleware does not reject unauthenticated preflights, and (3) response headers include all requiredAccess-Control-Allow-* values. When using credentials: include, Origin cannot be *; return the specific origin and addAccess-Control-Allow-Credentials: true.