TechBlog

CORSエラーの原因と解決方法:フロントエンドからAPIを叩くときのつまずきポイント

by あくえり
#CORS #JavaScript #API #セキュリティ #エラー解決
CORSエラー解決ガイド
目次

CORSエラーとは何か

フロントエンド開発でほぼ必ず一度は遭遇するのがこのエラーです。

Access to fetch at 'http://localhost:3001/api/users' from origin
'http://localhost:5173' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

CORS(Cross-Origin Resource Sharing)は、ブラウザのセキュリティ機能です。この仕組みを理解せずに対処法だけ覚えると、本番環境で同じ問題を繰り返すことになります。

Same-Origin Policy(同一オリジンポリシー)

CORSエラーの根本にあるのがSame-Origin Policyです。ブラウザは「異なるオリジンへのリクエスト」を制限する仕組みを持っています。

オリジンとは「スキーム + ホスト + ポート番号」の組み合わせです。

URLオリジン
http://localhost:5173http://localhost:5173
http://localhost:3001http://localhost:3001 ← 別オリジン(ポートが違う)
https://example.comhttps://example.com
https://api.example.comhttps://api.example.com ← 別オリジン(サブドメインが違う)

ローカル開発で「Viteのdev server(port 5173)からExpressのAPIサーバー(port 3001)にfetchしたら怒られた」というのが典型的なCORSエラーのシチュエーションです。

なぜこのような制限があるのか?

悪意のあるサイト(evil.com)が、あなたのSNSのCookieを使って勝手にAPIリクエストを送る「CSRF攻撃」を防ぐためです。Same-Origin Policyがなければ、あなたがログイン中のSNSのデータを別サイトのJavaScriptが盗み読みできてしまいます。

プリフライトリクエスト(Preflight Request)

GETPOST(Content-Type: application/x-www-form-urlencoded)は「シンプルリクエスト」として直接送信されますが、以下の条件に該当するリクエストは本番リクエストの前にOPTIONSメソッドによる「プリフライトリクエスト」が自動で送られます。

  • Content-Type: application/json(JSONを送るPOSTリクエスト)
  • PUT / DELETE / PATCH メソッド
  • カスタムヘッダーを付与する場合(Authorizationなど)
[ブラウザ] --OPTIONS--> [APIサーバー]
「このオリジンからJSON付きのPOSTを送っていいですか?」

[APIサーバー] --200 OK + CORSヘッダー--> [ブラウザ]
「いいですよ。許可するオリジンとメソッドはこちらです。」

[ブラウザ] --POST(本番リクエスト)--> [APIサーバー]
「では本番データを送ります。」

サーバーがOPTIONSリクエストを適切に処理しないと、本番リクエスト前に詰まってCORSエラーになります。

解決方法1:Expressサーバーにcorsミドルウェアを追加

バックエンドがExpressの場合、corsパッケージを使うのが最もシンプルな解決策です。

npm install cors
// server.js
import express from 'express';
import cors from 'cors';

const app = express();

// ❌ これはダメ:全オリジンを許可(本番では使わない)
// app.use(cors());

// ✅ 特定のオリジンのみ許可
app.use(cors({
  origin: ['http://localhost:5173', 'https://your-app.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,   // Cookie/Authorizationヘッダーを許可する場合
}));

app.use(express.json());

app.get('/api/users', (req, res) => {
  res.json([{ id: 1, name: '田中太郎' }]);
});

app.listen(3001, () => console.log('API server running on port 3001'));

credentials: trueを使う場合の注意点: origin*(全オリジン許可)は使えません。必ず具体的なオリジンを指定する必要があります。

解決方法2:Viteのプロキシを使う(開発環境)

開発時は、Viteのプロキシ機能でCORSを回避するのが手軽です。ブラウザからはViteのサーバーにアクセスしているように見えるため、Same-Origin Policyに引っかかりません。

// vite.config.js
import { defineConfig } from 'vite';

export default defineConfig({
  server: {
    proxy: {
      // /api で始まるパスをAPIサーバーに転送
      '/api': {
        target: 'http://localhost:3001',
        changeOrigin: true,
        // パスを書き換える場合(/api/users → /users)
        // rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
  },
});

このプロキシ設定後は、フロントエンドのfetchをこう書けます。

// フロントエンド
// ❌ 直接バックエンドのポートを指定しない
// const res = await fetch('http://localhost:3001/api/users');

// ✅ 同じオリジン(Vite)に向けてfetch → プロキシが転送
const res = await fetch('/api/users');
const users = await res.json();

解決方法3:CORSヘッダーを手動で設定する

corsパッケージを使わずにヘッダーを手書きする場合はこうなります。

// Express(手動でヘッダーを設定)
app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5173');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.setHeader('Access-Control-Allow-Credentials', 'true');

  // OPTIONSメソッド(プリフライト)に即レスポンスを返す
  if (req.method === 'OPTIONS') {
    return res.status(204).end();
  }
  next();
});

よくある落とし穴

Cookie(credentials)を送りたいのにエラーになる

fetchでCookieやAuthorizationヘッダーを含める場合は、クライアント側でもcredentials: 'include'の指定が必要です。

// ❌ デフォルトではCookieを送らない
const res = await fetch('http://localhost:3001/api/me');

// ✅ credentialsを明示的に指定
const res = await fetch('http://localhost:3001/api/me', {
  credentials: 'include',
});

この場合サーバー側もAccess-Control-Allow-Credentials: trueかつAccess-Control-Allow-Originにワイルドカード(*)不可というセットの条件になります。

本番環境でだけCORSエラーが出る

開発時にViteプロキシで解決していた場合、本番ビルドではプロキシが効きません。本番用の適切なNginxリバースプロキシ設定か、バックエンド側のCORSヘッダー設定が必要です。

# nginx.conf(リバースプロキシの例)
location /api {
    proxy_pass http://localhost:3001;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
}

ブラウザのキャッシュが原因のことも

OPTIONSレスポンスはAccess-Control-Max-Ageヘッダーでキャッシュされます。設定変更後もエラーが続く場合はブラウザのキャッシュをクリアするか、シークレットモードで確認しましょう。

プロを目指す人のためのTypeScript入門

TypeScriptの型システムをしっかり学べる定番書。fetch APIの型安全な使い方やエラーハンドリングも丁寧に解説されています。

※ アフィリエイトリンクを含みます

まとめ

CORSエラーを解決する方法をまとめます。

シチュエーション解決策
バックエンドを自分で管理しているcorsミドルウェアで許可オリジンを設定
開発環境のみ解決したいViteのserver.proxyを使う
外部APIでバックエンドを変更できない自前のプロキシサーバーを経由させる
サードパーティAPIキーを隠したいバックエンドを中継サーバーにする

根本的な解決はサーバー側のCORSヘッダー設定です。Access-Control-Allow-Originに適切なオリジンを指定し、本番環境では絶対に*(全許可)にしないよう注意しましょう。

共有: