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.

🌐 Navigateur🔒 Same-Origin Policy🟩 Express🐘 PHP⏱️ 18 min de lecture

Voici le message que tu as probablement sous les yeux dans la console de ton navigateur :

CORS policy blocked : « CORS policy blocked » : comprendre et corriger l’erreur
« CORS policy blocked » : comprendre et corriger l’erreur
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.
Texte

L’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.

CORS policy blocked : comment le navigateur autorise ou bloque une requête
Comment le navigateur décide d’autoriser ou de bloquer une requête vers une autre origine.
🎯
Ce que tu vas apprendre
  • 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.

PageRequête versMême origine ?Pourquoi
https://mon-site.behttps://mon-site.be/api✅ OuiTout est identique
http://localhost:5173http://localhost:3000/api❌ NonPort différent
https://mon-site.behttps://api.mon-site.be❌ NonSous-domaine différent
http://mon-site.behttps://mon-site.be❌ NonProtocole différent
https://mon-site.behttps://autre-site.com❌ NonDomaine 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.

💡
Le point le plus important de ce guide

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, authorization
HTTP

Le 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: 600
HTTP

Seulement si cette réponse convient, le navigateur envoie la vraie requête POST.

En-tête de réponseRôle
Access-Control-Allow-OriginL’origine autorisée (ou * pour toutes)
Access-Control-Allow-MethodsLes méthodes HTTP autorisées
Access-Control-Allow-HeadersLes en-têtes que le client peut envoyer
Access-Control-Allow-Credentialstrue pour autoriser l’envoi des cookies
Access-Control-Expose-HeadersLes en-têtes de réponse lisibles par le JavaScript
Access-Control-Max-AgeDuré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’erreurCauseCorrection
No ‘Access-Control-Allow-Origin’ header is presentL’API n’envoie aucun en-tête CORSConfigurer CORS sur l’API
The ‘Access-Control-Allow-Origin’ header has a value ‘…’ that is not equal to the supplied originL’origine autorisée ne correspond pasAjouter ton origine exacte (protocole + domaine + port)
Response to preflight request doesn’t pass access control checkLa 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 allowedEn-tête non autoriséL’ajouter à Access-Control-Allow-Headers
Method PUT is not allowedMéthode non autoriséeL’ajouter à Access-Control-Allow-Methods
must not be the wildcard ‘\‘ when the request’s credentials mode is ‘include’** utilisé avec des cookiesIndiquer l’origine exacte et Allow-Credentials: true
💡
Ouvre l’onglet Réseau, pas seulement la console

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 cors
Bash
// 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);
JavaScript

Le middleware cors répond automatiquement aux requêtes OPTIONS de preflight et ajoute les bons en-têtes à toutes les réponses.

⚠️
L’ordre des middlewares compte

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();
});
JavaScript
💡
Pourquoi l’en-tête Vary: Origin ?

Si 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']]);
PHP
⚠️
« Cannot modify header information – headers already sent »

Les 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]
Apache

Nginx

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;
}
Nginx
⚠️
Attention aux en-têtes en double

Si 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
});
JavaScript

Cô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
}));
JavaScript

Le 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).

🛡️
Ne renvoie jamais l’origine reçue sans la vérifier

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,
      },
    },
  },
});
JavaScript

Dans 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"
Bash

La 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.

💡
Pense aux environnements multiples

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 solutionPourquoi 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-securityMême problème, en plus dangereux si tu navigues avec ce Chrome.
Access-Control-Allow-Origin: * partout, avec les cookiesRefusé par les navigateurs, et la variante « renvoyer l’origine reçue » ouvre une vraie faille.
Un proxy CORS public trouvé sur InternetToutes tes données (et parfois tes jetons) transitent par le serveur d’un inconnu.
✅
Et quand l’API n’est pas la tienne ?

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.

CORS policy blocked : les points essentiels à retenir
Les points essentiels à retenir de ce guide.

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.

Comments

No comments yet. Why don’t you start the discussion?

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *