12 - CORS : comprendre et debugger les erreurs cross-origin
Ce que tu vas apprendre
- Ce qu'est la same-origin policy et pourquoi elle existe
- Le mecanisme des preflight requests (OPTIONS)
- Les headers CORS et comment les configurer
- Les erreurs CORS les plus courantes et comment les résoudre
Prerequisites
Avoir lu l'article sur le versioning. Savoir ce qu'est une requête HTTP et comprendre la notion de domaine/origin.
"Access to XMLHttpRequest at 'https://api.example.com' from origin 'http://localhost:3000' has been blocked by CORS policy." Si tu as deja fait du front, tu as vu ce message. Et tu as probablement installe cors avec Access-Control-Allow-Origin: * en te disant "ca marche, on verra plus tard". On va voir maintenant.
La same-origin policy
Le navigateur interdit par défaut les requêtes vers un domaine différent de celui de la page. https://app.example.com ne peut pas appeler https://api.example.com. C'est la same-origin policy.
Deux URLs ont la meme origin si elles partagent le meme protocole, domaine et port :
https://app.example.com:443 -- origin
https://app.example.com:443/users -- meme origin
http://app.example.com:443 -- different (protocole)
https://api.example.com:443 -- different (domaine)
https://app.example.com:8080 -- different (port)
Cette restriction existe pour empecher un site malveillant de faire des requêtes vers ta banque en utilisant tes cookies. C'est une protection du navigateur, pas du serveur. Un curl ou un appel serveur-to-serveur n'est pas concerne.
CORS : la soupape
CORS (Cross-Origin Resource Sharing) est le mecanisme qui permet au serveur de dire "j'autorise les requêtes venant de tel origin". Le serveur ajoute des headers dans sa réponse :
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Le navigateur lit ces headers et décidé s'il laisse passer la réponse ou s'il la bloque.
Les preflight requests
Pour certaines requêtes (PUT, DELETE, ou toute requête avec un header custom comme Authorization), le navigateur envoie d'abord une requête OPTIONS pour demander la permission :
OPTIONS /users/42 HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Authorization
Le serveur répond :
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: DELETE
Access-Control-Allow-Headers: Authorization
Access-Control-Max-Age: 86400
Access-Control-Max-Age dit au navigateur de cacher cette réponse pendant 24 heures. Sans ca, chaque requête généré un preflight supplementaire, ce qui double le nombre de requêtes.
Les requêtes "simples" (GET, POST avec Content-Type: application/x-www-form-urlencoded) ne declenchent pas de preflight. Mais des que tu ajoutes Content-Type: application/json ou Authorization, le preflight se déclenché.
Configuration en Express
typescriptimport cors from "cors";
// Dev : tout autoriser
app.use(cors());
// Production : origin specifique
app.use(cors({
origin: "https://app.paltemps.fr",
methods: ["GET", "POST", "PUT", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization"],
credentials: true,
maxAge: 86400
}));
Pour plusieurs origins :
typescriptconst allowedOrigins = [
"https://app.example.com",
"https://admin.example.com"
];
app.use(cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error("CORS not allowed"));
}
},
credentials: true
}));
Le !origin gere les requêtes sans origin (curl, Postman, requêtes serveur-to-serveur).
Configuration en Elysia
typescriptimport { Elysia } from "elysia";
import { cors } from "@elysiajs/cors";
new Elysia()
.use(cors({
origin: "https://app.example.com",
methods: ["GET", "POST", "PUT", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization"],
credentials: true
}))
.listen(3000);
Wildcard vs origin spécifique
Access-Control-Allow-Origin: * autorise tout le monde. Pratique en dev, dangereux en prod. Et surtout : ca ne marche pas avec credentials: true. Si tu as besoin d'envoyer des cookies ou le header Authorization, tu dois spécifier l'origin exacte.
# Ca ne marche PAS ensemble
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Le navigateur refusera la réponse. C'est une des erreurs CORS les plus frequentes.
Les erreurs courantes et leurs solutions
"No 'Access-Control-Allow-Origin' header" : ton serveur ne renvoie pas le header. Verifie que le middleware CORS est bien charge avant tes routes.
"The value of the 'Access-Control-Allow-Origin' header must not be the wildcard '*'" : tu utilises * avec credentials: true. Specifie l'origin exacte.
"Request header field authorization is not allowed" : tu n'as pas déclaré Authorization dans Access-Control-Allow-Headers.
La requête OPTIONS retourne 404 : ton framework ne gere pas automatiquement les OPTIONS. Ajoute un handler explicite ou vérifié la configuration du middleware.
Ca marche en Postman mais pas dans le navigateur : normal. Postman n'applique pas la same-origin policy. C'est une protection navigateur uniquement.
Un piège avec les proxys
En développement, beaucoup de frameworks front (Next.js, Vite) proposent un proxy intégré qui redirige /api/* vers ton backend. Ca elimine les problèmes CORS en dev puisque le front et l'API sont sur la meme origin (localhost). Mais en production, le proxy n'existe plus et CORS revient. Teste toujours ta configuration CORS dans un environnement proche de la prod.
Résumé
- CORS est un mecanisme navigateur qui permet au serveur d'autoriser les requêtes cross-origin
- Les preflight requests (OPTIONS) verifient les permissions avant la vraie requête
Access-Control-Allow-Origin: *ne fonctionne pas aveccredentials: true- Configure
Access-Control-Max-Agepour éviter les preflight a chaque requête - Teste ta config CORS en dehors de Postman, qui ignore la same-origin policy
Article précédent : Versioning Article suivant : Rate limiting