Ton fetch() fonctionne dans Postman mais échoue dans le navigateur avec un message rouge : « has been blocked by CORS policy ». C’est l’une des erreurs les plus frustrantes du développement web, et l’une des plus mal comprises. Dans ce guide, tu comprends pourquoi le navigateur bloque ta requête et tu apprends à corriger l’erreur CORS policy blocked proprement, côté serveur, en Node.js, en PHP, avec Apache ou Nginx.
Voici le message que tu as probablement sous les yeux dans la console de ton navigateur :

Access to fetch at 'http://localhost:3000/api/produits' from origin 'http://localhost:5173'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.TexteL’erreur CORS policy blocked déroute parce que tout semble correct : l’API répond bien dans Postman, avec curl ou quand tu ouvres l’URL directement. Le problème n’est pas ton code JavaScript, ni vraiment ton API : c’est une règle de sécurité du navigateur. Une fois que tu l’as comprise, la corriger prend deux minutes.

- Ce qu’est une « origine » et pourquoi le navigateur bloque certaines requêtes.
- La différence entre une requête simple et une requête preflight (OPTIONS).
- Corriger l’erreur CORS en Express, en PHP, avec Apache et avec Nginx.
- Gérer les cookies et l’authentification sans ouvrir de faille de sécurité.
- Les fausses solutions à éviter absolument.
Pourquoi le navigateur bloque ta requête
La politique de même origine
Les navigateurs appliquent une règle fondamentale : la politique de même origine (Same-Origin Policy). Un script chargé depuis un site ne peut pas lire librement les réponses d’un autre site.
Imagine que ce ne soit pas le cas : tu visites un site malveillant pendant que tu es connecté à ta banque dans un autre onglet. Le JavaScript de ce site pourrait appeler l’API de ta banque avec tes cookies et lire tes relevés. La politique de même origine empêche exactement ça.
Qu’est-ce qu’une origine ?
Une origine, c’est la combinaison de trois éléments : le protocole, le domaine et le port. Si un seul diffère, ce n’est plus la même origine.
| Page | Requête vers | Même origine ? | Pourquoi |
|---|---|---|---|
https://mon-site.be | https://mon-site.be/api | ✅ Oui | Tout est identique |
http://localhost:5173 | http://localhost:3000/api | ❌ Non | Port différent |
https://mon-site.be | https://api.mon-site.be | ❌ Non | Sous-domaine différent |
http://mon-site.be | https://mon-site.be | ❌ Non | Protocole différent |
https://mon-site.be | https://autre-site.com | ❌ Non | Domaine différent |
Voilà pourquoi l’erreur apparaît si souvent en développement : ton front (Vite, React, Vue) tourne sur localhost:5173 et ton API sur localhost:3000. Deux ports, donc deux origines.
CORS : la permission donnée par le serveur
CORS (Cross-Origin Resource Sharing) est le mécanisme qui permet d’assouplir cette règle. Le principe : c’est le serveur de l’API qui dit au navigateur quelles origines ont le droit de lire ses réponses, grâce à des en-têtes HTTP comme Access-Control-Allow-Origin.
L’erreur CORS policy blocked se corrige côté serveur (l’API), jamais côté client. Ton code fetch() est très probablement correct. C’est l’API qui doit envoyer les bons en-têtes pour autoriser ton front.
C’est aussi pour ça que Postman et curl ne voient jamais l’erreur : ce ne sont pas des navigateurs, ils n’appliquent pas la politique de même origine.
Requête simple ou requête preflight ?
Toutes les requêtes cross-origin ne sont pas traitées de la même façon.
Les requêtes simples
Une requête est « simple » si elle utilise GET, HEAD ou POST, sans en-têtes personnalisés, et avec un Content-Type parmi text/plain, multipart/form-data ou application/x-www-form-urlencoded. Le navigateur l’envoie directement, puis vérifie la réponse : si l’en-tête Access-Control-Allow-Origin est absent ou ne correspond pas, il bloque l’accès à la réponse (la requête a bien atteint le serveur).
Les requêtes avec preflight
Dès que ta requête utilise PUT, PATCH ou DELETE, un en-tête comme Authorization, ou un Content-Type: application/json (c’est-à-dire presque toutes les requêtes d’API modernes), le navigateur envoie d’abord une requête de vérification : le preflight, avec la méthode OPTIONS.
OPTIONS /api/produits HTTP/1.1
Origin: http://localhost:5173
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, authorizationHTTPLe serveur doit répondre en autorisant explicitement l’origine, la méthode et les en-têtes :
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 600HTTPSeulement si cette réponse convient, le navigateur envoie la vraie requête POST.
| En-tête de réponse | Rôle |
|---|---|
Access-Control-Allow-Origin | L’origine autorisée (ou * pour toutes) |
Access-Control-Allow-Methods | Les méthodes HTTP autorisées |
Access-Control-Allow-Headers | Les en-têtes que le client peut envoyer |
Access-Control-Allow-Credentials | true pour autoriser l’envoi des cookies |
Access-Control-Expose-Headers | Les en-têtes de réponse lisibles par le JavaScript |
Access-Control-Max-Age | Durée (en secondes) pendant laquelle le navigateur garde le preflight en cache |
Les variantes du message CORS policy blocked
Le message exact t’indique précisément ce qui manque. Voici comment le lire :
| Fin du message d’erreur | Cause | Correction |
|---|---|---|
| No ‘Access-Control-Allow-Origin’ header is present | L’API n’envoie aucun en-tête CORS | Configurer CORS sur l’API |
| The ‘Access-Control-Allow-Origin’ header has a value ‘…’ that is not equal to the supplied origin | L’origine autorisée ne correspond pas | Ajouter ton origine exacte (protocole + domaine + port) |
| Response to preflight request doesn’t pass access control check | La requête OPTIONS échoue (404, 401, 500, pas d’en-têtes) | Répondre aux requêtes OPTIONS avec les en-têtes CORS |
| Request header field authorization is not allowed | En-tête non autorisé | L’ajouter à Access-Control-Allow-Headers |
| Method PUT is not allowed | Méthode non autorisée | L’ajouter à Access-Control-Allow-Methods |
| must not be the wildcard ‘\‘ when the request’s credentials mode is ‘include’* | * utilisé avec des cookies | Indiquer l’origine exacte et Allow-Credentials: true |
Dans les outils de développement (F12), onglet Réseau, clique sur la requête bloquée (et sur la requête OPTIONS qui la précède, s’il y en a une). Tu vois les en-têtes envoyés et reçus. Souvent, tu découvres que le preflight renvoie une erreur 404 ou 401 : le vrai problème est là.
Corriger l’erreur CORS avec Node.js et Express
La solution la plus propre est le paquet officiel cors :
npm install corsBash// server.js
import express from 'express';
import cors from 'cors';
const app = express();
const ORIGINES_AUTORISEES = [
'http://localhost:5173', // front en développement (Vite)
'https://www.mon-site.be', // front en production
];
app.use(cors({
origin(origin, callback) {
// Les requêtes sans origine (curl, Postman, serveur à serveur) sont acceptées
if (!origin || ORIGINES_AUTORISEES.includes(origin)) {
return callback(null, true);
}
callback(new Error('Origine non autorisée par CORS'));
},
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 600,
}));
app.use(express.json());
app.get('/api/produits', (req, res) => {
res.json([{ id: 1, nom: 'Clavier' }]);
});
app.listen(3000);JavaScriptLe middleware cors répond automatiquement aux requêtes OPTIONS de preflight et ajoute les bons en-têtes à toutes les réponses.
Place app.use(cors(...)) avant tes routes et avant ton middleware d’authentification. Sinon, la requête OPTIONS (qui n’envoie jamais de jeton) est rejetée par l’authentification avec une erreur 401, et le preflight échoue.
Sans paquet : comprendre ce qui se passe
Pour comprendre le mécanisme (ou si tu ne peux pas installer de paquet), voici l’équivalent manuel :
const ORIGINES = new Set(['http://localhost:5173', 'https://www.mon-site.be']);
app.use((req, res, next) => {
const origine = req.headers.origin;
if (origine && ORIGINES.has(origine)) {
res.setHeader('Access-Control-Allow-Origin', origine);
res.setHeader('Vary', 'Origin'); // indispensable si la réponse dépend de l'origine
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
}
if (req.method === 'OPTIONS') {
return res.sendStatus(204); // réponse au preflight, sans exécuter la route
}
next();
});JavaScriptSi ta réponse change selon l’origine, un cache (navigateur, CDN, proxy) pourrait servir à un site la réponse préparée pour un autre, avec la mauvaise valeur d’Access-Control-Allow-Origin. Vary: Origin indique aux caches qu’ils doivent stocker une version par origine.
Corriger l’erreur CORS en PHP
En PHP, on envoie les en-têtes avec header(), avant tout affichage. Place ce code au tout début de ton point d’entrée d’API (ou dans un fichier inclus partout) :
<?php
// cors.php : à inclure en premier dans chaque point d'entrée de l'API
declare(strict_types=1);
$originesAutorisees = [
'http://localhost:5173',
'https://www.mon-site.be',
];
$origine = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origine, $originesAutorisees, true)) {
header('Access-Control-Allow-Origin: ' . $origine);
header('Vary: Origin');
header('Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Access-Control-Max-Age: 600');
}
// Réponse immédiate au preflight
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}PHP<?php
// api/produits.php
require __DIR__ . '/../cors.php';
header('Content-Type: application/json; charset=utf-8');
echo json_encode([['id' => 1, 'nom' => 'Clavier']]);PHPLes en-têtes doivent partir avant le moindre caractère de contenu : un echo, un espace avant <?php, ou un BOM UTF-8 dans le fichier suffisent à les bloquer. Inclus cors.php en toute première ligne.
Avec Laravel et Symfony, n’écris pas ce code à la main : Laravel intègre la configuration CORS dans config/cors.php, et Symfony propose le bundle nelmio/cors-bundle.
Corriger l’erreur CORS avec Apache ou Nginx
Si tu ne peux pas modifier le code de l’API (fichiers statiques, API tierce hébergée chez toi), configure les en-têtes au niveau du serveur web.
Apache (.htaccess)
Le module mod_headers doit être activé :
<IfModule mod_headers.c>
SetEnvIf Origin "^https://(www\.)?mon-site\.be$" ORIGINE_OK=$0
Header always set Access-Control-Allow-Origin "%{ORIGINE_OK}e" env=ORIGINE_OK
Header always set Vary "Origin"
Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
Header always set Access-Control-Allow-Headers "Content-Type, Authorization"
</IfModule>
# Répondre directement aux requêtes OPTIONS
RewriteEngine On
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^ - [R=204,L]ApacheNginx
location /api/ {
if ($http_origin ~* "^https://(www\.)?mon-site\.be$") {
add_header Access-Control-Allow-Origin $http_origin always;
add_header Vary Origin always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
}
if ($request_method = OPTIONS) {
return 204;
}
proxy_pass http://127.0.0.1:3000;
}NginxSi ton application et ton serveur web ajoutent tous les deux Access-Control-Allow-Origin, le navigateur reçoit deux valeurs et refuse la réponse (contains multiple values). Configure CORS à un seul endroit.
Cookies, sessions et authentification
Par défaut, fetch() n’envoie pas les cookies vers une autre origine. Si ton API utilise une session ou un cookie d’authentification, il faut l’autoriser des deux côtés.
Côté client :
const reponse = await fetch('https://api.mon-site.be/profil', {
credentials: 'include', // envoie les cookies vers l'autre origine
});JavaScriptCôté serveur (Express) :
app.use(cors({
origin: 'https://www.mon-site.be', // une origine précise, JAMAIS '*'
credentials: true, // ajoute Access-Control-Allow-Credentials: true
}));JavaScriptLe cookie lui-même doit être compatible avec un usage entre origines : SameSite=None; Secure s’il doit être envoyé depuis un autre site, ou SameSite=Lax si le front et l’API partagent le même site (par exemple www.mon-site.be et api.mon-site.be).
Une erreur classique consiste à écrire Access-Control-Allow-Origin: <valeur de l'en-tête Origin> pour « tout autoriser » tout en gardant les cookies. C’est encore pire que * : n’importe quel site peut alors lire les données de tes utilisateurs connectés. Vérifie toujours l’origine contre une liste blanche. C’est une forme de mauvaise configuration décrite dans l’OWASP Top 10.
En développement : le proxy, la solution la plus simple
Pendant le développement, tu peux éviter complètement CORS en faisant passer les appels d’API par le serveur de ton front. Avec Vite :
// vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
},
},
},
});JavaScriptDans ton code, appelle fetch('/api/produits') (sans le domaine). Pour le navigateur, la requête part vers localhost:5173 : même origine, plus de CORS. Vite la transmet ensuite à ton API. En production, le même résultat s’obtient en servant le front et l’API sous le même domaine (par exemple avec Nginx en proxy inverse).
Tester ta configuration CORS avec curl
Pas besoin de navigateur pour vérifier les en-têtes renvoyés par ton API. Avec curl, tu peux simuler exactement la requête de preflight que le navigateur enverrait :
curl -i -X OPTIONS http://localhost:3000/api/produits \
-H "Origin: http://localhost:5173" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: content-type, authorization"BashLa réponse doit contenir un statut 204 (ou 200) et les en-têtes Access-Control-Allow-Origin, Access-Control-Allow-Methods et Access-Control-Allow-Headers avec les bonnes valeurs. Teste aussi une origine non autorisée (par exemple -H "Origin: https://site-pirate.example") : l’en-tête Access-Control-Allow-Origin ne doit alors pas apparaître.
Ce test, rapide à automatiser, te permet de vérifier ta configuration après chaque déploiement, sans attendre qu’un utilisateur signale l’erreur.
Garde la liste des origines autorisées dans une variable d’environnement (CORS_ORIGINS=http://localhost:5173,https://www.mon-site.be) plutôt qu’en dur dans le code. Tu adaptes ainsi la configuration au développement, à la préproduction et à la production sans modifier une ligne.
Les fausses solutions à éviter
| Fausse solution | Pourquoi c’est une mauvaise idée |
|---|---|
fetch(url, { mode: 'no-cors' }) | L’erreur disparaît, mais la réponse devient opaque : ton JavaScript ne peut plus la lire. Ça ne corrige rien. |
| Extension « Allow CORS » dans le navigateur | Ça marche uniquement sur ton navigateur. Tes utilisateurs auront toujours l’erreur, et tu affaiblis ta propre sécurité. |
Lancer Chrome avec --disable-web-security | Même problème, en plus dangereux si tu navigues avec ce Chrome. |
Access-Control-Allow-Origin: * partout, avec les cookies | Refusé par les navigateurs, et la variante « renvoyer l’origine reçue » ouvre une vraie faille. |
| Un proxy CORS public trouvé sur Internet | Toutes tes données (et parfois tes jetons) transitent par le serveur d’un inconnu. |
Si tu appelles une API tierce qui n’autorise pas ton origine, la bonne solution est de passer par ton propre serveur : ton front appelle ton back-end, qui appelle l’API tierce (les serveurs ne sont pas soumis à CORS). Bonus : tes clés d’API restent secrètes, au lieu d’être visibles dans le code du navigateur.

FAQ
Pourquoi l’erreur CORS policy blocked n’apparaît-elle pas dans Postman ?
Parce que la politique de même origine est appliquée par les navigateurs uniquement. Postman, curl ou un serveur appellent l’API directement, sans restriction. Une API qui marche dans Postman peut donc parfaitement échouer dans le navigateur.
Access-Control-Allow-Origin: \* est-il dangereux ?
Pas pour une API publique et sans authentification (données météo, catalogue public) : c’est même le bon choix. Il devient un problème dès que l’API renvoie des données liées à un utilisateur. Dans ce cas, liste précisément les origines autorisées.
CORS protège-t-il mon API contre les attaques ?
Non. CORS protège les utilisateurs dans leur navigateur, pas ton serveur. N’importe qui peut appeler ton API avec curl. La sécurité de l’API repose sur l’authentification, les contrôles d’accès et la validation des données.
L’erreur apparaît seulement en production, pourquoi ?
Le plus souvent, l’origine de production n’est pas dans la liste blanche (attention au www, au https et aux ports), ou un proxy (Cloudflare, Nginx) supprime ou duplique les en-têtes. Compare les en-têtes reçus dans l’onglet Réseau en local et en production.
Conclusion
L’erreur CORS policy blocked n’est pas un bug : c’est le navigateur qui protège tes utilisateurs. La correction se fait toujours côté serveur, en autorisant précisément les origines qui ont le droit d’appeler ton API, en répondant aux requêtes OPTIONS de preflight et en gérant les cookies avec une origine exacte. En développement, un proxy Vite t’évite même complètement le problème.
Retiens surtout la méthode : lis la fin du message d’erreur, ouvre l’onglet Réseau, regarde la requête OPTIONS, et corrige l’en-tête manquant. Si ton API est en PHP, notre guide pour installer PHP 8.4 sous Windows t’aide à monter un environnement de test propre. La référence complète sur le sujet est la documentation CORS de MDN.

