Les erreurs CORS sont l'un des problèmes les plus frustrants du développement web. Vous voyez le redoutable message rouge dans la console, votre appel API échoue, et rien ne semble fonctionner. Ce guide vous aidera à comprendre exactement ce qu'est CORS, diagnostiquer votre erreur et la corriger.
Qu'est-ce que CORS ? (Explication en 30 secondes)
Le Cross-Origin Resource Sharing (CORS) est une fonctionnalité de sécurité du navigateur qui bloque les pages web qui tentent d'envoyer des requêtes vers un domaine différent de celui qui sert la page. Quand votre frontend http://localhost:3000 essaie de récupérer des données depuis http://api.example.com, le navigateur vérifie si le serveur API autorise explicitement cette requête cross-origin via des en-têtes HTTP spéciaux.
Comment fonctionne CORS : le flux
┌─────────────────┐ ┌─────────────────────┐
│ 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 │
└─────────────────┘ └──────────────────┘Quand CORS est correctement configuré, le serveur répond avec des en-têtes qui disent au navigateur : "Oui, j'autorise les requêtes de cette origine."
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:3000
Content-Type: application/json
{"data": "success"}Quand CORS n'est PAS configuré, le navigateur bloque la réponse (même si le serveur a traité la requête avec succès).
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 itLes 5 messages d'erreur CORS les plus courants
Erreur 1 : En-tête Access-Control-Allow-Origin absent
No 'Access-Control-Allow-Origin' header is present on the requested resource.Signification : Le serveur n'a pas inclus l'en-tête Access-Control-Allow-Origin. C'est l'erreur CORS la plus courante.
Erreur 2 : Wildcard avec identifiants
The value of the 'Access-Control-Allow-Origin' header must not be the wildcard '*' when the request's credentials mode is 'include'.Signification : Vous envoyez des cookies ou des en-têtes Authorization, mais le serveur répond avec *. Avec les identifiants, le serveur doit spécifier l'origine exacte.
Erreur 3 : Requête preflight échouée
Response to preflight request doesn't pass access control check.Signification : Le navigateur a envoyé une requête OPTIONS (preflight) et le serveur n'a pas géré correctement cette requête.
Erreur 4 : Méthode non autorisée
Method PUT is not allowed by Access-Control-Allow-Methods.Signification : L'en-tête Access-Control-Allow-Methods du serveur ne contient pas la méthode HTTP utilisée.
Erreur 5 : En-tête non autorisé
Request header field X-Custom-Header is not allowed by Access-Control-Allow-Headers.Signification : Le serveur n'a pas autorisé l'en-tête personnalisé dans Access-Control-Allow-Headers.
Organigramme de diagnostic : où est le problème ?
Utilisez cet arbre de décision pour identifier rapidement si votre problème CORS est frontend, backend ou proxy :
Étape 1 : Ouvrez DevTools > onglet Network. Voyez-vous une requête OPTIONS avant votre requête ?
OUI -> Le navigateur envoie une requête preflight. Vérifiez si la réponse OPTIONS a le statut 200 et contient les en-têtes CORS.
NON -> C'est une requête simple. Le problème est l'absence d'en-têtes CORS dans la réponse du serveur.
Étape 2 : L'erreur mentionne-t-elle "preflight" ?
OUI -> Problème backend. Votre serveur doit gérer les requêtes OPTIONS.
NON -> Vérifiez si vous utilisez des identifiants. Si oui, vous ne pouvez pas utiliser le wildcard (*).
Étape 3 : L'API fonctionne-t-elle en appel direct (curl, Postman) ?
OUI -> C'est bien un problème CORS (navigateur uniquement).
NON -> Ce n'est PAS un problème CORS. L'API elle-même a un problème.
Résumé : 90% des erreurs CORS se résolvent en configurant les bons en-têtes sur votre serveur backend.
Corrections côté serveur par framework
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)
}Configuration 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;
}
}Proxy inverse comme middleware CORS
Si vous ne pouvez pas modifier l'API backend (API tierce), utilisez un proxy inverse pour ajouter les en-têtes CORS.
Proxy inverse 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')Proxy inverse 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>Solutions pour le développement uniquement
Ces solutions sont uniquement pour le développement local. Ne jamais les utiliser en production.
Proxy Vite
Le proxy intégré de Vite redirige les requêtes du serveur de développement vers l'API, évitant CORS car le navigateur ne voit que des requêtes same-origin :
// 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')Proxy Webpack devServer
Create React App et les setups webpack supportent un proxy similaire :
// 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,
},
},
},
};Rewrites Next.js
Next.js peut proxifier les requêtes API via son propre serveur avec les rewrites :
// 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/usersPièges courants et cas particuliers
Piège 1 : Wildcard (*) avec identifiants
Quand le frontend envoie des identifiants, le serveur NE PEUT PAS utiliser *. Il doit renvoyer l'origine exacte :
FAUX (échouera avec les identifiants) :
// Frontend
fetch('https://api.example.com/data', {
credentials: 'include' // Sending cookies
});
// Backend response header:
Access-Control-Allow-Origin: * // FAILS! Cannot use * with credentialsCORRECT (origine dynamique pour les identifiants) :
// 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();
});Piège 2 : Absence de gestionnaire OPTIONS (preflight)
Les requêtes non-simples déclenchent un preflight OPTIONS. Si votre serveur renvoie 404 ou 405 pour OPTIONS, CORS échoue.
| 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();
});Piège 3 : Cookies et SameSite
Les cookies cross-origin nécessitent trois conditions :
// 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.Piège 4 : Cache des réponses preflight
Les requêtes preflight peuvent être lentes. Utilisez Access-Control-Max-Age pour les mettre en cache :
// 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.Note : Chrome limite à 7200 secondes (2 heures). Firefox limite à 86400 (24 heures).
Piège 5 : CORS et redirections
Si votre endpoint API redirige (301/302), les en-têtes CORS doivent être présents sur les DEUX réponses. Évitez les redirections dans les endpoints 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)Référence rapide : en-têtes CORS
| En-tête | Objectif | Valeur exemple |
|---|---|---|
| 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 |
Questions fréquemment posées
Peut-on désactiver CORS dans le navigateur ?
Techniquement oui, en lançant Chrome avec --disable-web-security, mais c'est extrêmement dangereux. Pour le développement, utilisez un proxy (Vite, webpack).
Pourquoi mon API fonctionne dans Postman mais pas dans le navigateur ?
CORS est un mécanisme de sécurité du navigateur uniquement. Postman, curl et les requêtes serveur-à-serveur n'appliquent pas CORS.
CORS est-il un correctif frontend ou backend ?
Presque toujours backend. Le serveur doit envoyer les bons en-têtes CORS. Le frontend ne peut pas contourner CORS. La seule solution frontend est d'utiliser un proxy.
Faut-il utiliser Access-Control-Allow-Origin: * en production ?
Uniquement pour les API vraiment publiques sans cookies ni authentification. Pour les API avec identifiants, spécifiez les origines exactes.
Qu'est-ce qu'une requête preflight ?
C'est une requête OPTIONS automatique envoyée par le navigateur avant la requête réelle. Elle se produit avec des en-têtes personnalisés, des méthodes non-simples ou un Content-Type spécial.
Comment corriger CORS pour les connexions WebSocket ?
Les WebSocket ne suivent pas les règles CORS. Le serveur doit valider l'en-tête Origin manuellement. Si une erreur CORS apparaît, c'est généralement le endpoint HTTP initial.