Il contesto
Nei miei progetti disaccoppiati tipo SyllVS, lo sviluppo avviene su due porte: il front React servito da Vite su localhost:5173, l'API Symfony su localhost:8000. Due origini diverse secondo il browser — e quindi, fin dal primo fetch verso l'API:
Access to fetch at 'http://localhost:8000/api/versements' from origin
'http://localhost:5173' has been blocked by CORS policy: Response to
preflight request doesn't pass access control check.
Capire il preflight
Per ogni richiesta «non semplice» (metodo PUT/DELETE, header Authorization o Content-Type: application/json), il browser invia prima una richiesta OPTIONS di ricognizione. Se il server non risponde con gli header CORS corretti, la vera richiesta non viene mai inviata. È per questo che si vede una richiesta OPTIONS fallita nella scheda network ancora prima del suo POST.
Insidia classica: il preflight OPTIONS non invia l'header Authorization. Se il firewall Symfony richiede un JWT su /api senza eccezione per OPTIONS, il preflight riceve un 401 e tutto viene bloccato.
La soluzione: nelmio/cors-bundle
composer require nelmio/cors-bundle# config/packages/nelmio_cors.yaml
nelmio_cors:
defaults:
origin_regex: true
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
allow_headers: ['Content-Type', 'Authorization']
expose_headers: ['Link']
max_age: 3600
paths:
'^/api/': null# .env.local
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'Il max_age: 3600 dice al browser di mettere in cache la risposta preflight per un'ora: una sola richiesta OPTIONS all'ora invece di una per ogni chiamata API.
L'insidia delle credentials
Se l'autenticazione passa per un cookie (refresh token httpOnly per esempio), servono due cose lato front e lato server:
// cote front : envoyer les cookies cross-origin
fetch('http://localhost:8000/api/token/refresh', {
method: 'POST',
credentials: 'include', // sans ca, le cookie ne part jamais
});# cote serveur
nelmio_cors:
defaults:
allow_credentials: true
# ATTENTION : avec allow_credentials, le wildcard '*' est interdit
# par la spec — il faut une origine explicite
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']È il punto che mi è costato più tempo: allow_origin: ['*'] + credentials: 'include' viene silenziosamente rifiutato dal browser. La specifica CORS impone un'origine esplicita ogni volta che ci sono credentials. L'errore in console non lo dice chiaramente, parla solo di policy failure.
E in produzione?
In prod, front e API vivono sullo stesso dominio dietro Apache (o su sottodomini precisi). La variabile d'ambiente basta a irrigidire:
# .env prod
CORS_ALLOW_ORIGIN='^https://(www\.)?monapp\.com$'
Mai regex lassiste in prod: un'origine CORS troppo aperta + credentials è una porta aperta al furto di sessione da qualsiasi sito.
Cosa mi porto a casa
- Una richiesta OPTIONS fallita nella scheda network = problema di preflight, la vera richiesta non è nemmeno partita.
- Il preflight non porta l'header Authorization: il firewall non deve mai richiedere un token sulle OPTIONS.
- allow_credentials: true vieta il wildcard * — origine esplicita obbligatoria, e il browser fallisce senza messaggio chiaro altrimenti.
- max_age sul preflight risparmia una richiesta OPTIONS per ogni chiamata API.
- La config CORS vive in una variabile d'ambiente: permissiva in dev, rigorosa in prod.