Le message ressemble à peu près à ceci, et la moitié qui compte, c’est la fin :

Access to fetch at 'https://api.example.com/orders' from origin
'https://app.example.com' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present on the requested resource.

Lisez-le comme une phrase sur celui qui a refusé. Le serveur a répondu. Votre requête a quitté le navigateur, atteint l’autre machine, et elle est revenue avec une réponse. Le navigateur a ensuite refusé de remettre cette réponse à votre JavaScript, parce que la réponse ne disait pas que votre origine avait le droit de la lire.

Ce seul fait écarte l’essentiel de ce que l’on essaie d’abord. Rien n’est cassé sur le réseau. Le serveur n’est pas tombé. Aucun pare-feu ne fait obstacle. La réponse est là, dans le navigateur, et le navigateur ne la transmettra pas.

Pourquoi le navigateur fait cela

Sans cela, n’importe quelle page que vous visitez pourrait discrètement adresser des requêtes à votre banque, à votre webmail ou aux outils internes de votre entreprise avec les cookies déjà présents dans votre navigateur, et en lire les réponses.

La règle est donc la suivante : JavaScript peut envoyer une requête vers une autre origine, mais ne peut lire la réponse que si le serveur qui répond dit que cette origine en a le droit. La permission doit venir du serveur interrogé, parce qu’il est le seul à savoir qui devrait la lire.

Deux conséquences qui surprennent :

  • Cela ne concerne que le JavaScript du navigateur. La même requête depuis curl, depuis Postman ou depuis votre propre backend fonctionne, parce qu’aucun d’eux n’est un navigateur qui protège la session de quelqu’un. Une requête qui passe dans Postman et échoue dans la page ne prouve pas que le serveur est cassé.
  • La désactiver dans votre navigateur ne corrige rien. Un drapeau ou une extension qui coupe la vérification fait disparaître l’erreur sur une seule machine, tandis que chaque visiteur continue de l’avoir. C’est un moyen de confirmer le diagnostic, pas un correctif, et utiliser un navigateur ainsi au quotidien est une très mauvaise idée.

Lire quelle règle a échoué

La fin du message nomme la règle, et il y en a trois que vous rencontrerez.

No ‘Access-Control-Allow-Origin’ header is present. (Aucun en-tête ‘Access-Control-Allow-Origin’ n’est présent.) Le cas simple. Le serveur n’a rien envoyé au sujet des permissions, donc le navigateur suppose qu’il n’y en a aucune.

Response to preflight request doesn’t pass access control check. (La réponse à la requête preflight ne passe pas le contrôle d’accès.) L’échec s’est produit avant même que votre requête soit envoyée. Tout ce qui dépasse une requête simple, un PUT ou un DELETE, un en-tête personnalisé comme Authorization, un type de contenu JSON, fait envoyer d’abord au navigateur une requête OPTIONS qui demande si la vraie est autorisée. Si cet OPTIONS renvoie un 404, un 500, une redirection, ou un 200 sans les bons en-têtes, la vraie requête n’a jamais lieu.

*The value of ‘Access-Control-Allow-Origin’ must not be the wildcard ‘’ when credentials mode is ‘include’.** (La valeur ne doit pas être le joker ‘*’ quand les identifiants sont envoyés.) Vous envoyez des cookies, et * n’est pas assez précis pour cela. Le serveur doit nommer votre origine exactement et ajouter Access-Control-Allow-Credentials: true.

Où va le correctif

Sur le serveur qui possède la ressource, dans tous les cas. Pas dans votre page, pas dans le navigateur, pas dans un proxy que vous placez devant votre propre front end.

Si c’est votre API, cela veut dire renvoyer Access-Control-Allow-Origin avec l’origine que vous voulez autoriser et, pour les preflights, répondre à OPTIONS avec les méthodes et les en-têtes autorisés et un statut 2xx. Si c’est l’API de quelqu’un d’autre et qu’elle n’autorise pas l’accès depuis un navigateur, alors l’accès depuis un navigateur n’est pas proposé : appelez-la depuis votre propre backend et laissez votre page dialoguer avec celui-ci.

Session Replay

Extension Chrome gratuite. Un clic sur la page qui se comporte mal capture la copie d'écran, la console et le journal réseau, et vous rend un lien à coller dans le ticket.

Installer l'extension

Sur localhost, là où la plupart des gens la rencontrent

http://localhost:3000 et http://localhost:8080 sont des origines différentes, donc un front end sur un port qui appelle une API sur un autre fait une requête inter-origines et a droit à tout le traitement.

La bonne réponse en développement est un proxy : votre serveur de développement transmet /api à l’API, si bien que le navigateur ne voit qu’une seule origine et que la question ne se pose jamais. Tout outil front end moderne le propose d’origine et cela tient en une ligne de configuration. La mauvaise réponse est de lancer Chrome avec la sécurité web désactivée, parce que le code ne fonctionne alors que pour ceux qui ont fait pareil.

Ce que votre code voit

Rien d’utile, et c’est justement ce qui rend le débogage déroutant.

Une réponse bloquée n’arrive pas sous la forme d’une erreur que vous pouvez inspecter. fetch() est rejeté avec le générique TypeError: Failed to fetch, sans statut et sans corps, parce que laisser la page lire pourquoi elle a été bloquée divulguerait précisément l’information que la règle existe pour protéger.

Le message CORS dans la console n’est donc pas une erreur que votre code a attrapée. C’est le navigateur qui vous dit, à vous le développeur, ce qu’il a fait. Votre code ne peut ni le voir, ni le journaliser, ni le remonter.

Cela vaut la peine de le savoir quand le signalement vient de quelqu’un d’autre. Un utilisateur qui tombe sur un échec CORS voit une fonctionnalité qui ne fait silencieusement rien, votre suivi des erreurs enregistre un Failed to fetch sans détail, et la phrase qui aurait tout expliqué a été écrite dans une console que personne n’a conservée.

Le retrouver dans un rapport

Deux endroits, et il vous les faut tous les deux.

La console porte le message CORS avec l’origine, l’URL et la règle en défaut. Le panneau réseau montre la requête elle-même, et, pour un échec de preflight, la requête OPTIONS posée au-dessus de la vraie, avec son propre statut. Ouvrez ses en-têtes de réponse : ce qui manque là est tout le diagnostic.

Si la requête n’apparaît pas du tout dans le panneau réseau, le navigateur a refusé avant l’envoi, ce qui désigne du contenu mixte ou une extension plutôt que CORS.

En bref

  • Le serveur a répondu ; le navigateur a refusé de laisser votre code lire la réponse.
  • Cela ne concerne que le JavaScript du navigateur, donc curl et Postman ne prouvent rien.
  • La fin du message nomme la règle en défaut. Lisez cela, pas le début.
  • Les échecs de preflight surviennent avant votre requête, sur un OPTIONS que le navigateur a envoyé pour vous.
  • Le correctif appartient au serveur qui répond. Désactiver la vérification en local est un diagnostic, pas un correctif.
  • Sur localhost, utilisez un proxy de développement pour qu’il n’y ait qu’une seule origine.