Votre frontend tourne sur http://localhost:5173, votre API sur http://localhost:4000, et l’appel fetch qui fonctionnait sans problème dans Postman échoue dans le navigateur avec un mur de texte rouge : Access to fetch at 'http://localhost:4000/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. Votre serveur a très bien pu traiter cette requête et renvoyer un 200. Le navigateur, lui, a refusé de transmettre la réponse à votre JavaScript.
Ce refus, c’est la same-origin policy. CORS est la façon dont un serveur indique au navigateur d’assouplir cette règle pour certains clients, et chaque solution ci-dessous se résume à un header de réponse que le navigateur n’a pas reçu.
Ce que bloque vraiment la same-origin policy
Une origin est la combinaison du schéma, de l’hôte et du port. http://localhost:5173 et http://localhost:4000 sont deux origins différentes parce que le port change, même si l’hôte et le schéma sont identiques. https://api.example.com et http://api.example.com sont aussi deux origins différentes, car le schéma change : l’une passe par TLS et l’autre non.
La same-origin policy est une règle du navigateur, pas du serveur. Un script qui tourne sur une origin ne peut pas lire la réponse d’une requête envoyée vers une origin différente, sauf si cette origin l’autorise explicitement. Pour une GET simple, la requête part quand même et le serveur la traite quand même : vous la voyez dans les logs du serveur et dans l’onglet Network avec un vrai code de statut. Ce que le navigateur retient, c’est le corps de la réponse. C’est pour ça qu’une erreur CORS ressemble à un problème côté client alors que le serveur ne signale rien d’anormal.
L’idée est d’empêcher une page sur evil.example d’utiliser votre session authentifiée dans le navigateur pour lire les réponses de l’API de votre banque. Sans cette règle, n’importe quel site que vous visitez pourrait envoyer des requêtes authentifiées vers n’importe quel autre site et lire les réponses.
Ce que CORS autorise, et ce qu’il ne protège pas
Cross-Origin Resource Sharing est une exception opt-in à cette règle. Le serveur ajoute des headers de réponse qui indiquent quelles origins peuvent lire ses réponses, et le navigateur les respecte au lieu de bloquer.
Le serveur ne refuse rien. Les headers CORS sont des instructions pour le navigateur, pas un contrôle d’accès. Une requête curl, un appel backend-to-backend ou une application mobile native atteignent le même endpoint sans aucune vérification CORS. Si votre API a besoin d’être protégée, c’est l’affaire de l’authentification et de l’autorisation.
Quand le navigateur envoie une requête preflight CORS
Le navigateur classe chaque requête cross-origin dans l’une de deux catégories, et une seule des deux demande la permission au préalable.
Une requête est simple quand toutes ces conditions sont réunies :
- La méthode est
GET,HEADouPOST. - Les seuls headers définis sont ceux que le navigateur ajoute automatiquement, plus une courte liste autorisée (
Accept,Accept-Language,Content-Language,Content-Type,Range). - Si
Content-Typeest défini, sa valeur estapplication/x-www-form-urlencoded,multipart/form-dataoutext/plain.
Une requête simple part directement, et le navigateur vérifie les headers de réponse pour décider si votre script reçoit le body. Tout le reste est preflighted : un PUT ou un DELETE, un body JSON (Content-Type: application/json), ou un header personnalisé comme Authorization ou X-API-Key. Avant d’envoyer la requête réelle, le navigateur envoie une requête OPTIONS pour demander au serveur ce qu’il autorise :
| |
Le serveur répond avec ce qu’il autorise. Ce n’est que si cette réponse couvre la requête réelle que le navigateur l’envoie :
| |
Access-Control-Max-Age indique combien de secondes le navigateur peut mettre cette réponse en cache et sauter le preflight à la prochaine requête équivalente. Une API JSON qui envoie un bearer token déclenche un preflight à presque chaque appel, donc ce header économise un aller-retour à chaque fois. Les navigateurs le plafonnent : Chromium ne le met pas en cache au-delà de 7200 secondes, quelle que soit la valeur envoyée.
Headers de réponse CORS et le rôle de chacun
| Header | Ce qu’il fait |
|---|---|
Access-Control-Allow-Origin | L’origin autorisée à lire la réponse : une origin littérale, ou * pour n’importe quelle origin. * est rejeté sur les requêtes avec identifiants. |
Access-Control-Allow-Methods | Les méthodes autorisées sur cette ressource, renvoyées dans la réponse au preflight. |
Access-Control-Allow-Headers | Les headers de requête autorisés, renvoyés dans la réponse au preflight. Authorization doit être listé nommément ; * ne le couvre pas. |
Access-Control-Allow-Credentials | true permet au navigateur d’exposer la réponse à une requête avec identifiants (cookies, authentification HTTP). Exige une origin littérale, pas *. |
Access-Control-Expose-Headers | Les headers de réponse, au-delà de la safelist par défaut, que JavaScript est autorisé à lire. |
La règle sur les identifiants est celle qui pose le plus de problèmes. « Identifiants » désigne ici les cookies et l’authentification HTTP gérée par le navigateur, envoyés quand vous appelez fetch(url, { credentials: 'include' }). Sur une telle requête, le serveur doit envoyer Access-Control-Allow-Credentials: true et indiquer l’origin exacte dans Access-Control-Allow-Origin. Une réponse avec identifiants et Access-Control-Allow-Origin: * est rejetée même si tous les autres headers sont corrects.
Un bearer token que vous définissez vous-même dans un header Authorization n’est pas un identifiant au sens CORS. C’est un header personnalisé : il déclenche un preflight et doit figurer dans Access-Control-Allow-Headers, mais il n’a pas besoin de Allow-Credentials.
Comment résoudre les erreurs CORS avec Express
Le paquet cors définit ces headers à votre place.
| |
| |
En production, vous avez souvent plus d’un frontend légitime. Passez un tableau ou une fonction plutôt qu’une simple chaîne :
| |
Monté avec app.use, cors() répond au preflight OPTIONS de chaque route. Si vous ne le montez que sur des routes spécifiques utilisant une méthode autre que GET, ajoutez app.options("/api/users", cors()) pour que le preflight de ce chemin reçoive aussi une réponse.
Comment activer CORS dans ASP.NET Core
Enregistrez une policy nommée dans Program.cs et appelez UseCors entre UseRouting et UseAuthorization. L’ordre compte : un preflight ne porte aucune authentification, donc si l’autorisation s’exécute avant, elle rejette la requête OPTIONS avant que CORS puisse répondre.
| |
WithOrigins n’accepte que des origins littérales, pour la même raison que le header brut. Combinez AllowCredentials() avec AllowAnyOrigin() et ASP.NET Core lève une InvalidOperationException plutôt que d’envoyer une réponse que le navigateur rejetterait de toute façon. Pour un contrôle par endpoint, placez [EnableCors("_allowFrontend")] sur un controller ou une action et appelez app.UseCors() sans nom de policy : le middleware doit tout de même être présent dans le pipeline pour que l’attribut prenne effet.
Comment éviter CORS en développement avec un proxy Vite
Sur votre machine, la solution la plus simple est de ne plus faire de requête cross-origin du tout. Vite, webpack-dev-server et la plupart des autres serveurs de développement peuvent servir de proxy vers votre backend pour un chemin donné, de sorte que le navigateur ne parle jamais qu’à une seule origin :
| |
Une requête fetch vers /api/users part maintenant vers le serveur de développement Vite sur http://localhost:5173, qui la transmet à http://localhost:4000 et relaie la réponse. Le navigateur ne voit qu’une seule origin, donc il n’y a rien à bloquer pour CORS.
C’est une commodité de développement. En production, l’équivalent est un reverse proxy qui sert le frontend et l’API depuis une seule origin publique, ce qui supprime complètement le besoin de headers CORS sur cette API.
Solutions de contournement CORS à éviter
Chacune de ces méthodes fait disparaître le texte rouge pour vous et laisse intact le vrai problème, un header que votre API n’envoie pas :
Access-Control-Allow-Origin: *sur un endpoint qui utilise des cookies. Le navigateur rejette la réponse avec identifiants, donc cela ne corrige même pas l’erreur. Retirez les cookies pour faire fonctionner le wildcard et vous obtenez un endpoint qui répond à n’importe quel site sur internet sans aucune vérification d’origin.- Une extension de navigateur « CORS unblock ». Elle modifie les réponses à l’intérieur de votre propre navigateur. L’erreur disparaît pour vous et reste pour chaque utilisateur réel, y compris un collègue qui teste le staging depuis une autre machine.
- Lancer Chrome avec
--disable-web-security. Cela désactive la same-origin policy pour tous les onglets de cette instance du navigateur, pas seulement votre application, tant qu’elle reste ouverte. Un onglet oublié sur un site malveillant réduit à néant exactement la protection que cette politique est censée offrir.
Quelle solution CORS choisir
Si l’erreur n’apparaît qu’en local, contre un backend sur un port différent, utilisez le proxy du serveur de développement et évitez complètement la configuration CORS.
Si de vrais utilisateurs vont appeler votre API en cross-origin depuis un domaine frontend séparé ou un site tiers, définissez Access-Control-Allow-Origin avec les origins exactes que vous comptez servir. Ajoutez Access-Control-Allow-Credentials uniquement si le navigateur envoie des cookies. Ouvrez ensuite l’onglet Network de DevTools et vérifiez la requête OPTIONS avant de supposer que tout a fonctionné : un 2xx avec Allow-Origin, Allow-Methods et Allow-Headers couvrant votre requête réelle signifie que la requête suivante passera.