AIにフロントとAPIをまとめて書かせて、ローカルで動かした瞬間にこれが出る。
Access to fetch at 'http://localhost:8000/api/items' from origin 'http://localhost:3000'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.
これをそのまま貼って「CORSエラーが出ました」と伝えると、だいたい Access-Control-Allow-Origin: * を足す修正が返ってきます。ローカルでは直ります。そして本番で別の形で困ります。
この記事は、CORSエラーを直すときにAIへ何を渡すと、その場しのぎではない修正が返ってくるかの話です。
前提:このエラーはフロント側の問題ではない
まず切り分けから。CORSはブラウザの仕組みで、別オリジンへのリクエストに対して、サーバー側が「このオリジンからは読んでよい」と明示していないと、レスポンスをJavaScriptに渡さないというものです。
ここで重要なのは3点です。
- 多くの場合、リクエスト自体はサーバーに届いている。ブラウザがレスポンスをJavaScriptに渡さないだけで、サーバーのログには残る(※後述のプリフライトが挟まる条件では、プリフライトが失敗した時点で本体のリクエストは送られません)
- だから許可を出す場所はサーバー側。フロント側の書き方を変えるだけでは解決しない(Cookieを使う場合だけ、後述のとおりフロントにも
credentials: 'include'の指定が要ります) -
オリジンは スキーム + ホスト + ポート の3点セット。
http://localhost:3000とhttp://localhost:8000は、ホストが同じでもポートが違うので別オリジン
3番目でつまずく人が多い印象があります。「同じlocalhostなのになぜ」となりますが、ポートが違えば別オリジンです。http:// と https:// も同様に別扱いになります。
「そのまま貼る」と何が起きるか
エラー文だけ貼ると、AIは最短で消える方法を返します。典型的なのがこれです。
// Express の例
app.use(cors()); // = すべてのオリジンを許可
これは Access-Control-Allow-Origin: * を返す設定です。エラーは消えます。ただし、この状態には2つの性質があります。
- どのサイトからでもこのAPIを叩いて中身を読める(公開APIなら妥当だが、そうでないなら意図と違う)
-
credentials: 'include'(Cookie送信)と併用できない。ワイルドカードとクレデンシャル付きリクエストの組み合わせはブラウザが拒否する
2番目は、ログイン機能を後から足したときに効いてきます。Access-Control-Allow-Origin: * のまま credentials: 'include' を付けると、仕様上かならずブラウザに弾かれます。「CORSは前に直したはずなのに、ログインを実装したらまた出た」となるのはこれが理由です。
渡すべき情報は4つ
同じエラーでも、次の4点を添えると返ってくる修正が変わります。
CORSエラーが出ています。以下の前提で、暫定ではない修正を提案してください。
1. フロント: Next.js(http://localhost:3000)/本番は https://example.com
2. API: Express(http://localhost:8000)/本番は https://api.example.com
3. Cookieによるセッション認証を使う予定がある(credentials: 'include')
4. 許可したいオリジンは上記の2つだけ。それ以外からは読ませたくない
ワイルドカード(*)を使わない形でお願いします。
併せて、なぜその設定が必要かも1行ずつ説明してください。
ポイントは、「ワイルドカードを使わない」と明示することと、Cookie認証の予定を先に伝えることです。この2つがないと、AIは目の前のエラーだけを消しにいきます。予定を伝えておけば、最初から併用可能な形で提案されます。
返ってくる形
上の条件を渡すと、だいたいこういう方向の修正になります。
const allowedOrigins = [
'http://localhost:3000',
'https://example.com',
];
app.use(cors({
origin: function (origin, callback) {
// origin が undefined なのは同一オリジン・curl など。用途に応じて扱いを決める
if (!origin || allowedOrigins.includes(origin)) {
return callback(null, true);
}
return callback(new Error('Not allowed by CORS'));
},
credentials: true, // Cookie を伴うリクエストを許可
}));
callback(new Error(...)) は cors の公式ドキュメントどおりの書き方ですが、Express のエラーハンドラに流れるので実際には 500 が返ります(開発中はスタックトレース付き)。単に「許可しない」だけで済ませたい場合は callback(null, false) を返す手もあります。この場合は Access-Control-Allow-Origin ヘッダが付かないだけで、サーバー自体は通常どおり応答します。
ここでは2つの設定が、別々の仕事をしています。
-
originに配列や関数を渡すと、corsはリクエストのOriginヘッダを照合し、許可されたときだけそのオリジンをそのままAccess-Control-Allow-Originに返します(あわせてVary: Originも付きます) -
credentials: trueが足すのはAccess-Control-Allow-Credentials: trueヘッダだけです
つまり、ワイルドカードが消えるのは origin を指定したからであって、credentials: true の効果ではありません。ここを取り違えると、origin を '*' のままにして credentials: true だけ足す——という、ブラウザに必ず弾かれる設定を書いてしまいます。Cookie認証と併用するには、この2つが両方必要です。
なお、!origin を許可するかどうかはセキュリティ境界の話ではありません。CORS はブラウザの仕組みなので、curl からのリクエストは元々このチェックを通りません。APIを守るのはサーバー側の認証であって、CORS設定ではないという前提は押さえておいてください。(細かい点として、file:// などからは Origin: null という文字列が来るので !origin では拾えません)
フロント側は fetch にこれを足します。
const res = await fetch('http://localhost:8000/api/items', {
credentials: 'include',
});
確認するところ
修正したら、ブラウザの開発者ツール(Network タブ)で該当リクエストのレスポンスヘッダを見ます。
| ヘッダ | 期待する値 |
|---|---|
Access-Control-Allow-Origin |
* ではなく、自分のオリジンが具体的に入っている |
Access-Control-Allow-Credentials |
true(Cookieを使う場合) |
ここが * のままなら、効いていないのは credentials: true ではなく origin の指定です(未設定か '*' のままになっています)。逆に Access-Control-Allow-Credentials の行が出ていないなら、そちらが credentials: true の付け忘れです。
また、条件によっては本体の前に OPTIONS メソッドのプリフライトリクエストが飛びます。飛ぶのは次のどれかに当てはまるときです。
-
GET/HEAD/POST以外のメソッド(PUT/DELETE/PATCHなど) -
Authorizationなどの独自ヘッダを付けている -
Content-Typeがapplication/json(safelist はapplication/x-www-form-urlencoded/multipart/form-data/text/plainの3つだけ)
3つ目が見落とされがちです。JSONをPOSTするAPIは、それだけでプリフライトが飛びます。この記事の例のような構成なら、ほぼ確実に OPTIONS が挟まると思っておいてください。
プリフライトで落ちている場合は、許可メソッド・許可ヘッダを明示する形になります(先ほどの設定を置き換える形です。二重に登録しないでください)。なお cors の既定値は PUT/DELETE を含み、許可ヘッダもリクエスト内容を反射するので、明示指定はむしろ許可範囲を絞る方向の変更になります。
app.use(cors({
origin: allowedOrigins,
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));
AIに投げる前に決めておくこと
まとめると、CORSで詰まったときにAIへ渡す前に、自分の側で決めておくのは次の4つです。
- どのオリジンから叩かれるのか(開発と本番の両方)
- Cookieを使うのか、使わないのか
- 公開APIなのか、自分のフロントからだけ叩くAPIなのか
- GET以外のメソッドを使うのか
もう一点、本番構成で引っかかりやすいところを。記事の例(example.com と api.example.com)はドメインが同じなので Cookie はそのまま送られますが、フロントとAPIで登録可能ドメインが変わる構成(Vercel と Railway に分ける、など)では、CORS を正しく直しても Cookie に SameSite=None; Secure が付いていないと、そもそも送られません。CORS の話とは別レイヤーなので、切り分けて確認してください。
この4つが決まっていれば、エラー文を貼るだけの依頼と比べて、返ってくるものの寿命がかなり変わります。逆にこれが決まっていないと、AIは決めようがないので、一番無難で一番緩い設定を出してきます。
エラーを消すことと、設定を決めることは別の作業です。前者だけをAIに投げていると、後者がずっと未決のまま残ります。
未経験者向けの講座を運営しています
未経験から Next.js + Supabase + Claude Code で Webアプリを公開するまで を、全20セッションで体系化した教材です。Claude Code を学習パートナーにする CLAUDE.md / Skills 設計までセットで含みます。
- 無料体験版(git clone してすぐ動く・最初の数セッション分・⭐ Star もよろしくお願いします)→ https://github.com/ayies128/next-ai-camp-trial
- 教材完全版+月5,500円のメンタリング(全20セッション+チャット質問し放題)→ https://menta.work/plan/20251?ref=qiita
- YouTube『AIエンジニア情報局』(AI×開発ニュースを1本5分でキャッチアップできる別運営チャンネル・無料)→ https://www.youtube.com/channel/UC1rXVD9WYsQPQEWZyd-A1KA/?ref=qiita
※ この教材は Qiita 読者の方には易しすぎる内容なので、初心者の知り合いへの紹介や社内研修の参考としてどうぞ。