Erreur CORS : pourquoi curl fonctionne mais le navigateur bloque

L’API répond dans le terminal, mais votre interface affiche une erreur CORS. Ce décalage ne prouve pas que le navigateur se trompe : curl ne fait pas respecter la politique CORS du navigateur. Il faut comparer l’origine de la page, la prérequête éventuelle et les en-têtes de la réponse, pas ouvrir toutes les origines au hasard.

Pourquoi curl fonctionne alors que fetch échoue

CORS, pour Cross-Origin Resource Sharing, permet à un serveur d’indiquer quelles autres origines peuvent lire ses réponses depuis un navigateur. Une origine associe protocole, hôte et port : https://app.example et https://api.example sont différentes. En local, http://localhost:5173 et http://localhost:3000 le sont aussi. Changer seulement le chemin ne change pas l’origine.

Un code HTTP 200 dans le terminal confirme une réponse HTTP, pas l’autorisation de la lire depuis votre page. À l’inverse, un message CORS ne permet pas à lui seul de conclure que le serveur n’a rien exécuté. Certaines requêtes partent directement ; le navigateur peut ensuite masquer leur réponse au JavaScript.

1. Observer la requête réelle avant de modifier la configuration

  1. Ouvrez les outils de développement, onglets Réseau et Console. Relevez le message exact, l’URL appelée, la méthode et la valeur de Origin.
  2. Repérez une éventuelle requête OPTIONS, puis la requête métier. Notez pour chacune le statut et les en-têtes de réponse.
  3. Comparez le navigateur et le terminal : même URL finale, même méthode, mêmes en-têtes utiles ? Un appel anonyme en ligne de commande n’est pas équivalent à un appel avec session.
  4. Regardez les journaux de l’application et du reverse proxy au même instant. Avant de transmettre une capture ou un export réseau, retirez cookies, jetons, données personnelles et corps sensibles.

Trois pistes à distinguer : si OPTIONS échoue et que la requête métier ne part pas, examinez la prérequête ; si la requête métier reçoit une réponse mais reste illisible, examinez ses en-têtes CORS ; si la connexion échoue aussi hors navigateur, recherchez d’abord la panne HTTP, réseau ou TLS.

2. Vérifier la prérequête OPTIONS

Un POST inter-origines avec Content-Type: application/json, ou un appel comportant Authorization, déclenche normalement une prérequête. Le navigateur demande si la méthode et les en-têtes prévus sont autorisés. Cette vérification ne contient pas le corps du futur POST.

Voici un exemple à adapter uniquement à une API que vous administrez, de préférence en environnement de test. Les domaines sont fictifs. La commande envoie un OPTIONS, pas le POST annoncé :

curl -i -X OPTIONS 'https://api.example/v1/dossiers' \
  -H 'Origin: https://app.example' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: content-type'

Pour cet exemple sans session, une réponse d’autorisation pourrait être :

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Content-Type
Vary: Origin

Un simple 204 ne suffit pas : les valeurs doivent correspondre à la demande. Si votre appel réel ajoute Authorization, annoncez aussi cet en-tête dans le test et vérifiez qu’il est explicitement autorisé côté serveur. N’ajoutez pas de vrai jeton pour tester la prérequête.

Le standard prévoit une prérequête sans identifiants d’authentification. Si une passerelle impose la session de l’utilisateur à OPTIONS et renvoie 401, prévoyez un traitement spécifique de la prérequête pour les origines autorisées. Conservez l’authentification et les contrôles de droits sur la véritable opération.

3. Contrôler aussi la réponse métier et les cookies

Après un OPTIONS accepté, la réponse au POST doit elle aussi comporter Access-Control-Allow-Origin avec la bonne valeur. Vérifiez également les réponses d’erreur : un 401 ou un 500 sans ces en-têtes peut devenir illisible pour le JavaScript et masquer le problème initial.

Pour une session par cookie entre origines, le client peut avoir besoin de credentials: "include". Le serveur doit alors autoriser explicitement l’origine et répondre avec Access-Control-Allow-Credentials: true, sur la prérequête s’il y en a une et sur la réponse métier. La combinaison avec Access-Control-Allow-Origin: * est refusée. Les règles des cookies, dont SameSite et les restrictions des cookies tiers, restent applicables.

Si vous choisissez dynamiquement l’origine parmi une liste autorisée, renvoyez une seule origine validée et ajoutez Origin à l’en-tête Vary existant. Ne remplacez pas ses autres valeurs. Faites porter la configuration par une couche identifiée : application ou proxy, pour éviter des en-têtes contradictoires.

Les faux correctifs à éviter

  • Ajouter les en-têtes Allow côté JavaScript. Ce sont des en-têtes de réponse que doit fournir le serveur de l’API.
  • Passer fetch en mode: "no-cors". La réponse devient opaque : votre code ne peut pas lire son corps ni ses en-têtes. Ce n’est pas une solution pour récupérer du JSON.
  • Renvoyer aveuglément Origin. Comparez l’origine complète à une liste de confiance, sans recherche approximative dans une chaîne.
  • Désactiver la sécurité du navigateur. Le défaut restera présent chez les utilisateurs et vous perdrez un garde-fou pendant vos essais.

Conclusion : valider le scénario, pas seulement le statut HTTP

Après correction, testez depuis l’origine autorisée avec le vrai parcours utilisateur, puis vérifiez qu’une origine non autorisée ne peut pas lire la réponse. Testez aussi une erreur d’authentification. Le résultat attendu est précis : la bonne page accède aux bonnes données, les autres non. CORS ne remplace ni l’authentification, ni les autorisations, ni la protection CSRF ; un client hors navigateur peut toujours appeler l’API.

Sources

Publications similaires