CORSエラーはWeb開発で最もフラストレーションの溜まる問題の一つです。コンソールに赤いエラーメッセージが表示され、API呼び出しが失敗し、何も機能しないように見えます。このガイドでは、CORSとは何かを正確に理解し、具体的なエラーを診断し、適切なソリューションで修正する方法を解説します。
CORSとは?(30秒で解説)
Cross-Origin Resource Sharing(CORS)は、Webページがページを提供しているドメインとは異なるドメインにリクエストを送ることをブロックするブラウザのセキュリティ機能です。フロントエンドの http://localhost:3000 が http://api.example.com からデータを取得しようとすると、ブラウザはAPIサーバーが特別なHTTPヘッダーでこのクロスオリジンリクエストを明示的に許可しているか確認します。
CORSの仕組み:フロー図
┌─────────────────┐ ┌─────────────────────┐
│ Browser │ │ API Server │
│ (Frontend) │ │ (Backend) │
│ │ │ │
│ http://localhost │ ──────> │ https://api.example │
│ :3000 │ fetch │ .com │
│ │ │ │
└─────────────────┘ └─────────────────────┘
│
┌───────────────┴───────────────┐
│ │
CORS Headers No CORS Headers
Present? Present?
│ │
▼ ▼
┌─────────────────┐ ┌──────────────────┐
│ Response sent │ │ Browser BLOCKS │
│ to JavaScript │ │ the response │
└─────────────────┘ └──────────────────┘CORSが正しく設定されている場合、サーバーはブラウザに「はい、このオリジンからのリクエストを許可します」と伝えるヘッダーで応答します。
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:3000
Content-Type: application/json
{"data": "success"}CORSが設定されていない場合、ブラウザはレスポンスをブロックします(サーバーがリクエストを正常に処理していても)。
HTTP/1.1 200 OK
Content-Type: application/json
# No Access-Control-Allow-Origin header!
{"data": "success"} <-- Browser received this but BLOCKS JavaScript from reading it最も一般的な5つのCORSエラーメッセージ
エラー1:Access-Control-Allow-Originヘッダーがない
Access to fetch at 'https://api.example.com/data' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.意味:サーバーがレスポンスにAccess-Control-Allow-Originヘッダーを含めていません。最も一般的なCORSエラーで、サーバーにCORS設定が全くないことを意味します。
エラー2:ワイルドカードとクレデンシャルの競合
The value of the 'Access-Control-Allow-Origin' header must not be the wildcard '*' when the request's credentials mode is 'include'.意味:Cookie や Authorization ヘッダー(credentials: "include")を送信していますが、サーバーが Access-Control-Allow-Origin: * で応答しています。クレデンシャルが関わる場合、ワイルドカードではなく正確なオリジンを指定する必要があります。
エラー3:プリフライトリクエストの失敗
Response to preflight request doesn't pass access control check.意味:実際のリクエストの前に、ブラウザが権限を確認するOPTIONSリクエスト(プリフライト)を送信しました。サーバーがOPTIONSメソッドを処理していないか、プリフライトレスポンスにCORSヘッダーが含まれていません。
エラー4:メソッドが許可されていない
Method PUT is not allowed by Access-Control-Allow-Methods in preflight response.意味:サーバーの Access-Control-Allow-Methods ヘッダーに使用しているHTTPメソッド(PUT、DELETE、PATCHなど)が含まれていません。
エラー5:ヘッダーが許可されていない
Request header field X-Custom-Header is not allowed by Access-Control-Allow-Headers.意味:サーバーが Access-Control-Allow-Headers でカスタムヘッダーを明示的に許可していません。
診断フローチャート:問題はどこにあるか?
この決定木を使って、CORSの問題がフロントエンド、バックエンド、プロキシ設定のどこにあるかを素早く特定します:
ステップ1:DevTools > Networkタブを開きます。実際のリクエストの前にOPTIONSリクエストが見えますか?
はい -> プリフライトリクエストが送信されています。OPTIONSレスポンスがステータス200でCORSヘッダーを含んでいるか確認してください。
いいえ -> シンプルリクエストです。サーバーレスポンスにCORSヘッダーがないのが問題です。
ステップ2:エラーメッセージに「preflight」と書かれていますか?
はい -> バックエンドの問題です。サーバーでOPTIONSリクエストを処理し、CORSヘッダーを返す必要があります。
いいえ -> クレデンシャルを使用しているか確認してください。使用している場合、ワイルドカード(*)は使えません。
ステップ3:APIを直接呼び出す(curl や Postman)と動作しますか?
はい -> CORSの問題(ブラウザのみ)であることが確認できます。
いいえ -> CORSの問題ではありません。API自体に問題があります。
まとめ:CORSエラーの90%はバックエンドサーバーで正しいヘッダーを設定することで解決できます。
フレームワーク別サーバーサイド修正
Express.js (Node.js)
// Method 1: Using the cors package (recommended)
npm install cors
const express = require('express');
const cors = require('cors');
const app = express();
// Allow specific origins
app.use(cors({
origin: ['http://localhost:3000', 'https://yoursite.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400 // Cache preflight for 24 hours
}));
// Method 2: Manual middleware
app.use((req, res, next) => {
const allowedOrigins = ['http://localhost:3000', 'https://yoursite.com'];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,PATCH');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization');
res.setHeader('Access-Control-Allow-Credentials', 'true');
// Handle preflight
if (req.method === 'OPTIONS') {
res.setHeader('Access-Control-Max-Age', '86400');
return res.status(204).end();
}
next();
});Django (Python)
# Install django-cors-headers
pip install django-cors-headers
# settings.py
INSTALLED_APPS = [
...
'corsheaders',
...
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', # Must be FIRST
'django.middleware.common.CommonMiddleware',
...
]
# Option 1: Allow specific origins
CORS_ALLOWED_ORIGINS = [
'http://localhost:3000',
'https://yoursite.com',
]
# Option 2: Allow all origins (dev only!)
# CORS_ALLOW_ALL_ORIGINS = True
# Allow credentials (cookies, auth headers)
CORS_ALLOW_CREDENTIALS = True
# Allowed headers
CORS_ALLOW_HEADERS = [
'content-type',
'authorization',
'x-requested-with',
]
# Allowed methods
CORS_ALLOW_METHODS = [
'GET',
'POST',
'PUT',
'PATCH',
'DELETE',
'OPTIONS',
]Flask (Python)
# Install flask-cors
pip install flask-cors
from flask import Flask
from flask_cors import CORS
app = Flask(__name__)
# Option 1: Allow specific origins
CORS(app, origins=['http://localhost:3000', 'https://yoursite.com'],
supports_credentials=True,
allow_headers=['Content-Type', 'Authorization'],
methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])
# Option 2: Manual approach
@app.after_request
def after_request(response):
origin = request.headers.get('Origin')
allowed = ['http://localhost:3000', 'https://yoursite.com']
if origin in allowed:
response.headers['Access-Control-Allow-Origin'] = origin
response.headers['Access-Control-Allow-Methods'] = 'GET,POST,PUT,DELETE'
response.headers['Access-Control-Allow-Headers'] = 'Content-Type,Authorization'
response.headers['Access-Control-Allow-Credentials'] = 'true'
return responseGo (net/http)
package main
import (
"net/http"
"strings"
)
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
allowedOrigins := map[string]bool{
"http://localhost:3000": true,
"https://yoursite.com": true,
}
origin := r.Header.Get("Origin")
if allowedOrigins[origin] {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Methods",
strings.Join([]string{"GET","POST","PUT","DELETE","PATCH"}, ","))
w.Header().Set("Access-Control-Allow-Headers",
"Content-Type, Authorization")
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Max-Age", "86400")
}
// Handle preflight
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api/data", handleData)
handler := corsMiddleware(mux)
http.ListenAndServe(":8080", handler)
}Nginx設定
server {
listen 80;
server_name api.example.com;
# CORS headers for all locations
set $cors_origin "";
if ($http_origin ~* "^https?://(localhost:3000|yoursite\.com)$") {
set $cors_origin $http_origin;
}
location /api/ {
# CORS headers
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' 86400 always;
# Handle preflight requests
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' 86400;
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain';
return 204;
}
proxy_pass http://backend:8080;
}
}リバースプロキシをCORSミドルウェアとして使用
バックエンドAPIを変更できない場合(サードパーティAPIなど)、リバースプロキシを使用してCORSヘッダーを追加します。
Nginx リバースプロキシ
# Nginx as CORS proxy for a third-party API
server {
listen 80;
server_name cors-proxy.yoursite.com;
location /proxy/ {
# Add CORS headers
add_header 'Access-Control-Allow-Origin' 'https://yoursite.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://yoursite.com';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
add_header 'Access-Control-Max-Age' 86400;
return 204;
}
# Strip /proxy/ prefix and forward to third-party API
rewrite ^/proxy/(.*) /$1 break;
proxy_pass https://third-party-api.com;
proxy_set_header Host third-party-api.com;
proxy_set_header X-Real-IP $remote_addr;
}
}
# Usage: fetch('https://cors-proxy.yoursite.com/proxy/endpoint')Apache リバースプロキシ
# Apache as CORS proxy
<VirtualHost *:80>
ServerName cors-proxy.yoursite.com
# Enable required modules
# a2enmod proxy proxy_http headers rewrite
<Location "/proxy/">
Header always set Access-Control-Allow-Origin "https://yoursite.com"
Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
Header always set Access-Control-Allow-Headers "Content-Type, Authorization"
Header always set Access-Control-Allow-Credentials "true"
# Handle preflight
RewriteEngine On
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=204,L]
# Proxy to backend
ProxyPass "http://backend:8080/"
ProxyPassReverse "http://backend:8080/"
</Location>
</VirtualHost>開発環境専用ソリューション
これらのソリューションはローカル開発専用です。本番環境では絶対に使用しないでください。
Vite プロキシ
Viteの組み込みプロキシは開発サーバーからAPIにリクエストを転送し、ブラウザは同一オリジンのリクエストしか見ないためCORSを完全に回避します:
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
server: {
proxy: {
// Proxy /api requests to backend
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
// Optional: rewrite path
// rewrite: (path) => path.replace(/^\/api/, ''),
},
// Proxy with WebSocket support
'/ws': {
target: 'ws://localhost:8080',
ws: true,
},
},
},
});
// In your code, use relative URLs:
// fetch('/api/users') instead of fetch('http://localhost:8080/api/users')Webpack devServer プロキシ
Create React Appなどwebpackベースのセットアップも同様のプロキシをサポートします:
// package.json (Create React App)
{
"proxy": "http://localhost:8080"
}
// Or for more control, create src/setupProxy.js
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function(app) {
app.use(
'/api',
createProxyMiddleware({
target: 'http://localhost:8080',
changeOrigin: true,
})
);
};
// webpack.config.js (manual setup)
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
};Next.js リライト
Next.jsはrewritesを使用してAPIリクエストを自身のサーバー経由でプロキシできます:
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
async rewrites() {
return [
{
source: '/api/:path*',
destination: 'http://localhost:8080/api/:path*',
},
];
},
};
module.exports = nextConfig;
// In your code:
// fetch('/api/users') -- proxied to http://localhost:8080/api/usersよくある落とし穴とエッジケース
落とし穴1:ワイルドカード(*)とクレデンシャル
フロントエンドがクレデンシャルを送信する場合、サーバーは Access-Control-Allow-Origin: * を使用できません。正確なオリジンを返す必要があります:
間違い(クレデンシャルで失敗):
// Frontend
fetch('https://api.example.com/data', {
credentials: 'include' // Sending cookies
});
// Backend response header:
Access-Control-Allow-Origin: * // FAILS! Cannot use * with credentials正解(クレデンシャル用の動的オリジン):
// Backend (Express.js example)
app.use((req, res, next) => {
// Echo back the exact origin
const origin = req.headers.origin;
const allowedOrigins = ['http://localhost:3000', 'https://yoursite.com'];
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin); // Exact origin
}
res.setHeader('Access-Control-Allow-Credentials', 'true');
next();
});落とし穴2:OPTIONSハンドラーの欠如(プリフライト)
非シンプルリクエストはプリフライトOPTIONSリクエストを発生させます。サーバーがOPTIONSに404や405を返すと、実際のリクエストが送信される前にCORSが失敗します。
| Trigger | Example |
|---|---|
| Custom headers | Authorization, X-Request-ID, X-API-Key |
| Non-simple methods | PUT, DELETE, PATCH |
| Non-simple Content-Type | application/json, application/xml |
| ReadableStream body | Streaming request body |
// Ensure your server handles OPTIONS for all API routes:
app.options('*', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*');
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,PATCH');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization');
res.setHeader('Access-Control-Max-Age', '86400');
res.status(204).end();
});落とし穴3:CookieとSameSite
クロスオリジンCookieには3つの条件が必要です:
// 1. Frontend: include credentials
fetch('https://api.example.com/data', {
credentials: 'include'
});
// 2. Backend: allow credentials + exact origin
res.setHeader('Access-Control-Allow-Origin', 'https://yoursite.com'); // NOT *
res.setHeader('Access-Control-Allow-Credentials', 'true');
// 3. Cookie must have correct attributes
Set-Cookie: session=abc123; SameSite=None; Secure; HttpOnly; Path=/
// IMPORTANT: SameSite=None requires Secure (HTTPS).
// This means cross-origin cookies do NOT work on HTTP (localhost).
// For local dev, use a proxy instead of cross-origin cookies.落とし穴4:プリフライトレスポンスのキャッシュ
プリフライト(OPTIONS)リクエストは遅い場合があります。Access-Control-Max-Ageでキャッシュします:
// Server response header:
Access-Control-Max-Age: 86400 // Cache preflight for 24 hours
// Without this header, the browser sends a preflight OPTIONS
// request before EVERY non-simple request, adding latency.
// With caching, the browser remembers the CORS permissions
// and skips the preflight for subsequent requests.注意:Chromeの上限は7200秒(2時間)、Firefoxの上限は86400秒(24時間)です。
落とし穴5:リダイレクトとCORS
APIエンドポイントがリダイレクト(301/302)する場合、CORSヘッダーは元のレスポンスとリダイレクト先の両方に存在する必要があります。APIエンドポイントではリダイレクトを避けてください。
// PROBLEM: API endpoint redirects
// GET https://api.example.com/users -> 301 -> https://api.example.com/v2/users
// CORS headers must be on BOTH responses!
// SOLUTION: Avoid redirects in API endpoints
// Option 1: Update the frontend URL
fetch('https://api.example.com/v2/users'); // Use final URL directly
// Option 2: Make the redirect include CORS headers
// (requires server configuration on both the old and new endpoint)クイックリファレンス:CORSヘッダー
| ヘッダー | 目的 | 値の例 |
|---|---|---|
| Access-Control-Allow-Origin | Which origins can access the resource | https://yoursite.com or * |
| Access-Control-Allow-Methods | Which HTTP methods are allowed | GET, POST, PUT, DELETE |
| Access-Control-Allow-Headers | Which request headers are allowed | Content-Type, Authorization |
| Access-Control-Allow-Credentials | Whether cookies/auth can be sent | true |
| Access-Control-Max-Age | How long to cache preflight (seconds) | 86400 |
| Access-Control-Expose-Headers | Which response headers JS can read | X-Total-Count, X-Request-ID |
よくある質問
ブラウザでCORSを無効にできますか?
技術的には可能です(Chromeを--disable-web-securityフラグで起動)が、非常に危険でデバッグ以外には使用すべきではありません。開発環境ではプロキシ(Vite、webpack)を使用してください。
PostmanではAPIが動くのにブラウザでは動かないのはなぜ?
CORSはブラウザ専用のセキュリティメカニズムです。Postman、curl、サーバー間リクエストはCORSを強制しません。ブラウザはユーザーを保護するためにCORSヘッダーを確認します。
CORSはフロントエンドとバックエンドどちらの修正?
ほぼ常にバックエンドの修正です。サーバーが正しいCORSレスポンスヘッダーを送信する必要があります。フロントエンドはCORSをバイパスできません。唯一の回避策はプロキシの使用です。
本番環境で Access-Control-Allow-Origin: * を使うべき?
Cookieや認証を使用しない完全に公開されたAPIのみ。クレデンシャルが必要なAPIでは正確なオリジンを指定する必要があります。
プリフライトリクエストとは何ですか?
プリフライトリクエストは、ブラウザが実際のリクエストの前に自動送信するOPTIONSリクエストです。カスタムヘッダー、非シンプルHTTPメソッド、特殊なContent-Typeを使用する場合に発生します。
WebSocket接続のCORSを修正するには?
WebSocket接続はCORSルールに従いません。初期HTTPアップグレードハンドシェイクにOriginヘッダーが含まれますが、サーバーが手動で検証する必要があります。CORSエラーが出る場合、通常は初期HTTPハンドシェイクエンドポイントの問題です。