Votre API Symfony répond quand vous l’appelez depuis le terminal, mais le navigateur refuse le même appel depuis votre front. Dans l’onglet Réseau, vous voyez une requête OPTIONS et parfois aucun POST derrière. C’est cette première réponse qu’il faut regarder avant de modifier le code JavaScript.
Je prends un cas précis : une application web sur http://localhost:5173 appelle une API Symfony sur http://localhost:8000. La route POST /api/preview existe déjà et renvoie un aperçu JSON sans enregistrer de données. L’exemple n’utilise ni cookie de session ni jeton d’accès. Il s’applique à une API dont vous contrôlez la configuration.
Lire la requête que le navigateur envoie avant le POST
Ouvrez les outils de développement, onglet Réseau (Network), puis refaites l’action dans votre front de développement. Cherchez preview. Sélectionnez la ligne OPTIONS et ouvrez En-têtes (Headers). Si la méthode n’est pas visible dans la liste, elle apparaît dans la section Général de la requête.

Le navigateur compare le protocole, l’hôte et le port. Ici, les ports diffèrent : ce sont deux origines, même si tout tourne sur le même ordinateur. Pour ce POST interorigine avec Content-Type: application/json, le navigateur vérifie d’abord si le serveur accepte l’origine, la méthode et l’en-tête demandé. C’est la requête préalable, ou preflight.
Dans les en-têtes de la requête OPTIONS, cherchez ces trois lignes. Les valeurs sont celles de notre exemple, pas des paramètres à ajouter au JavaScript :
Origin: http://localhost:5173
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type
Regardez ensuite les en-têtes de réponse. Le navigateur cherche notamment une autorisation de lecture pour l’origine du front. Si la vérification échoue, il n’envoie pas le POST de cet exemple. La spécification Fetch distingue les en-têtes de la vérification préalable de ceux de la vraie réponse.
Autoriser le front sur la route Symfony
Dans le répertoire de votre projet Symfony, installez le bundle s’il n’est pas déjà présent. La commande suppose que Composer est installé. Symfony Flex active normalement le bundle ; sinon, vérifiez sa déclaration dans config/bundles.php en suivant la documentation d’installation.
composer require nelmio/cors-bundle
Pour un projet de démonstration sans règle CORS existante, placez cet exemple dans config/packages/dev/nelmio_cors.yaml. Si votre projet possède déjà une configuration Nelmio, adaptez la règle existante pour cette route dans l’environnement de développement. Ne dupliquez pas les clés YAML et vérifiez qu’une règle plus générale placée avant ne couvre pas déjà ce chemin.
nelmio_cors:
paths:
'^/api/preview$':
origin_regex: false
allow_origin: ['http://localhost:5173']
allow_methods: ['POST']
allow_headers: ['Content-Type']
allow_credentials: false
max_age: 0
La règle correspond exactement à /api/preview, sans barre oblique finale. Elle autorise le POST demandé, et le bundle traite la requête préalable OPTIONS. Copiez l’origine affichée dans DevTools, sans chemin ni barre finale : 127.0.0.1 et localhost sont deux hôtes distincts. La référence de configuration Nelmio explique les règles par chemin et les valeurs héritées.
Pour savoir ce que Symfony a réellement chargé dans l’environnement où vous testez, affichez la configuration effective :
php bin/console debug:config nelmio_cors
Cette commande ne vérifie pas ce que répond un proxy situé devant Symfony. Pour ça, interrogez l’URL exacte que le navigateur utilise.
Vérifier les deux réponses, pas seulement le statut
La commande suivante contrôle les en-têtes du preflight sur votre serveur local. Utilisez curl.exe à la place de curl dans Windows PowerShell si curl y désigne un alias. Il n’y a ni mot de passe ni jeton à ajouter pour ce contrôle.
curl -i -X OPTIONS "http://localhost:8000/api/preview" -H "Origin: http://localhost:5173" -H "Access-Control-Request-Method: POST" -H "Access-Control-Request-Headers: content-type"
Attendez un statut 2xx et des en-têtes qui couvrent les valeurs demandées. Sur une réponse correcte, cherchez notamment :
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: content-type
Retournez ensuite dans le navigateur et refaites l’action sur votre route de test. Pour isoler l’appel du reste de votre interface, voici un exemple à exécuter dans la console DevTools du front local ouvert sur le port 5173. Adaptez le corps JSON aux données attendues par votre route d’aperçu.
const response = await fetch('http://localhost:8000/api/preview', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Aperçu de test' })
});
console.log(response.status, await response.text());
Vérifiez que le POST a bien été envoyé et qu’il renvoie lui aussi Access-Control-Allow-Origin. Un OPTIONS réussi ne garantit pas que le front peut lire la réponse finale. Si le POST affiche un 400 et que JavaScript peut lire ce statut, l’échange CORS fonctionne : il reste à corriger les données envoyées.
Quand la requête échoue encore, remonter à la bonne couche
| Dans l’onglet Réseau | Quoi vérifier |
|---|---|
OPTIONS renvoie 301 ou 302. | Utiliser l’URL finale correcte dans le front : protocole, chemin et barre finale. Examiner l’en-tête Location. |
OPTIONS renvoie 401, 403 ou 405. | Vérifier qui répond : proxy, passerelle ou application. La requête préalable n’envoie pas les identifiants de l’appel final. Corriger son traitement dans la couche concernée, sans retirer les contrôles sur le POST. |
OPTIONS renvoie 200 mais le POST n’apparaît pas. | Comparer l’origine, la méthode et les noms d’en-têtes demandés avec les valeurs autorisées. Un statut seul ne valide rien. |
| Le POST part, mais JavaScript ne peut pas lire sa réponse. | Examiner les en-têtes du POST, y compris sur les réponses d’erreur. Vérifier qu’une seule couche écrit la politique CORS et qu’elle ne produit pas deux valeurs concurrentes. |
Gardez les protections du navigateur actives. mode: 'no-cors' ne rend pas la réponse JSON lisible, et ajouter Access-Control-Allow-Origin à la requête du front ne change pas la politique du serveur. En production, autorisez l’origine HTTPS réelle du front pour les routes qui en ont besoin. L’API doit toujours contrôler qui peut faire quoi, qu’un navigateur applique CORS ou non.
Questions fréquentes
Pourquoi curl fonctionne-t-il alors que le navigateur bloque ?
curl affiche la réponse HTTP sans appliquer la politique de partage du navigateur. Il permet de lire les en-têtes, mais le contrôle final doit se faire depuis le front.
Je ne vois pas de requête OPTIONS. Est-ce normal ?
Oui, si l’appel ne nécessite pas de preflight ou si le navigateur a déjà mémorisé son résultat. Vérifiez d’abord si le POST apparaît et quels en-têtes sa réponse contient.
Faut-il changer cette règle pour des cookies de session ?
Oui, l’exemple les exclut. Un flux avec cookies doit être conçu à part : origine explicite, paramètres de cookies adaptés, protection CSRF et autorisation des requêtes avec identifiants. N’ouvrez pas la règle à tous les sites.
Une origine autorisée donne-t-elle accès à toute l’API ?
Non. Elle indique au navigateur quelles réponses il peut partager avec cette origine. L’authentification et les autorisations restent vérifiées par l’API pour chaque opération protégée.
Les guides officiels à garder sous la main
À lire aussi
ARTICLE · 2 SEPT. Symfony Messenger : rejouer proprement un message échoué
ARTICLE · 16 AOÛT Composer : Résoudre un conflit Composer sans supprimer composer.lock
ARTICLE · 14 OCT. Mon-ip.dev : je vous présente mon site gratuit pour comprendre et diagnostiquer votre réseau

Commentaires
0