# « No ‘Access-Control-Allow-Origin’ header » : déboguer CORS en headless

> L'erreur CORS la plus fréquente en headless, décortiquée : ce qu'elle signifie vraiment, comment vérifier les en-têtes preflight, et où corriger le problème.

- Auteur : Clément Hadrot
- Publié le : 2022-06-09
- Mis à jour le : 2022-06-09
- Catégorie : Headless &amp; API
- URL : https://wpmoderne.dev.wordpress-developpement.fr/headless/no-access-control-allow-origin-deboguer-cors-headless/

## L’essentiel

- L'erreur vient du navigateur, jamais du serveur lui-même
- La requête preflight OPTIONS doit renvoyer les bons en-têtes
- Correction possible côté WordPress ou côté serveur web

« 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 :

> L'essentiel à retenir : L'erreur vient du navigateur, jamais du serveur lui-même ; La requête preflight OPTIONS doit renvoyer les bons en-têtes ; Correction possible côté WordPress ou côté serveur web

```
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 `OPTIONS` avec `curl -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-Origin` en 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: true` et l'absence du caractère générique `*` dans `Access-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.
