« Access to fetch at ‘https://exemple.fr/wp-json/wp/v2/posts’ from origin ‘https://front.exemple.fr’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. » Ce message, ou une variante très proche, revient dans presque tous les projets headless où le front et l’API WordPress ne sont pas servis depuis le même nom de domaine. C’est un rite de passage, mais qui reste mal compris par beaucoup de développeurs découvrant le headless.
Cet article décortique cette erreur symptôme par symptôme, sans reprendre les bases générales de l’authentification headless, qui font l’objet d’un traitement séparé.
Ce que dit vraiment l’erreur
Premier point à comprendre, contre-intuitif pour beaucoup : cette erreur ne signifie pas que la requête a échoué côté serveur. Dans la majorité des cas, WordPress a bien traité la requête et renvoyé une réponse valide ; c’est le navigateur qui refuse de la transmettre au code JavaScript qui l’a demandée, parce que le serveur n’a pas explicitement autorisé l’origine du front à lire cette réponse. C’est une protection de sécurité intégrée au navigateur, pas une erreur réseau.
Une conséquence pratique : ouvrir l’URL de l’API directement dans le navigateur fonctionne toujours sans erreur CORS (ce n’est pas une requête cross-origin dans ce contexte), ce qui peut faire croire à tort que « l’API fonctionne » alors que le problème persiste bel et bien pour les appels effectués depuis le front.
La requête preflight OPTIONS
Pour certaines requêtes (notamment les POST avec un en-tête Content-Type: application/json, ou toute requête portant un en-tête Authorization), le navigateur envoie d’abord une requête OPTIONS de vérification, appelée preflight, avant la requête réelle. Cette requête interroge le serveur pour savoir s’il autorise l’origine, la méthode et les en-têtes demandés.
curl -X OPTIONS https://exemple.fr/wp-json/wp/v2/comments \
-H "Origin: https://front.exemple.fr" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type" \
-i
La réponse doit contenir des en-têtes précis. Leur absence, même partielle, bloque la requête réelle qui suit :

Access-Control-Allow-Origin: https://front.exemple.fr
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true
Corriger côté WordPress
WordPress gère déjà une partie de la logique CORS pour ses propres routes via le filtre rest_pre_serve_request, mais uniquement pour une origine générique et sans authentification par cookie. Pour un front sur un domaine distinct, il faut souvent l’étendre :
add_action( 'rest_api_init', function () {
remove_filter( 'rest_pre_serve_request', 'rest_send_cors_headers' );
add_filter( 'rest_pre_serve_request', function ( $value ) {
$origines_autorisees = array( 'https://front.exemple.fr' );
$origine = get_http_origin();
if ( in_array( $origine, $origines_autorisees, true ) ) {
header( 'Access-Control-Allow-Origin: ' . esc_url_raw( $origine ) );
header( 'Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS' );
header( 'Access-Control-Allow-Headers: Content-Type, Authorization' );
header( 'Access-Control-Allow-Credentials: true' );
}
return $value;
} );
}, 15 );
Remplacer le filtre par défaut plutôt que d’ajouter des en-têtes en plus évite les doublons d’en-têtes Access-Control-Allow-Origin, une autre source fréquente d’échec silencieux : un navigateur qui reçoit deux valeurs différentes pour ce même en-tête rejette la requête, même si l’une des deux valeurs était correcte.
Corriger côté serveur web
Si WordPress est servi derrière Nginx ou Apache avec un cache de page agressif, il arrive que les en-têtes CORS définis en PHP soient ignorés pour les réponses mises en cache par le serveur web lui-même. Dans ce cas, il faut définir les en-têtes directement au niveau du serveur, pour la route /wp-json/ :
location /wp-json/ {
add_header 'Access-Control-Allow-Origin' 'https://front.exemple.fr' 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;
}
}
Checklist de diagnostic
- Vérifier la réponse à la requête
OPTIONSaveccurl -i, pas seulement la requête finale : c’est souvent là que le problème se situe. - Vérifier qu’aucun en-tête
Access-Control-Allow-Originen double n’est renvoyé (une fois par WordPress, une fois par le serveur web ou un plugin de cache). - Si des cookies d’authentification sont utilisés, vérifier la présence de
Access-Control-Allow-Credentials: trueet l’absence du caractère générique*dansAccess-Control-Allow-Origin(les deux sont incompatibles selon la spécification). - Vérifier que l’environnement de développement local (souvent sur un port différent, donc une origine différente) est bien inclus dans la liste des origines autorisées.
| Symptôme observé | Cause probable |
|---|---|
| Erreur uniquement sur les requêtes POST, pas GET | Preflight OPTIONS mal configuré |
| Erreur uniquement en local, pas en production | Origine de développement absente de la liste autorisée |
| Fonctionne parfois, échoue parfois de façon aléatoire | En-têtes CORS écrasés par un cache de page côté serveur |
Face à une erreur CORS, je commence toujours par inspecter la requête preflight dans l’onglet réseau du navigateur avant de toucher au code : la réponse OPTIONS dit presque toujours exactement quel en-tête manque, ce qui évite de corriger au hasard.
En résumé
L’erreur CORS ne signale pas un échec de la requête côté serveur, mais un refus du navigateur de transmettre la réponse au front. La correction passe par une inspection systématique de la requête preflight OPTIONS, puis par l’ajout des en-têtes appropriés côté WordPress ou côté serveur web, en évitant soigneusement les doublons d’en-têtes qui provoquent un nouvel échec, différent mais tout aussi silencieux.