Il frontend gira su http://localhost:5173, l’API su http://localhost:4000, e la fetch che funzionava perfettamente in Postman fallisce nel browser con un muro di testo rosso: Access to fetch at 'http://localhost:4000/api/users' from origin 'http://localhost:5173' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. Il server potrebbe aver gestito la richiesta e restituito un 200. Il browser ha semplicemente rifiutato di consegnare la risposta al tuo JavaScript.
Quel rifiuto ha un nome: same-origin policy. CORS è il modo in cui un server dice al browser di allentarla per chiamanti specifici, e ogni soluzione qui sotto si riduce a un header di risposta che il browser non ha ricevuto.
Cosa blocca davvero la same-origin policy
Un’origin è la combinazione di schema, host e porta. http://localhost:5173 e http://localhost:4000 sono origin diverse perché cambia la porta, anche se host e schema coincidono. https://api.example.com e http://api.example.com sono origin diverse anche loro, perché cambia lo schema: una passa per TLS e l’altra no.
La same-origin policy è una regola del browser, non del server. Uno script che gira su un’origin non può leggere la risposta di una richiesta fatta verso un’origin diversa, a meno che quell’origin non lo permetta esplicitamente. Per una GET semplice, la richiesta parte comunque e il server la processa: la vedi nei log del server e nel tab Network con un codice di stato reale. Quello che il browser trattiene è il corpo della risposta. Per questo un errore CORS sembra un problema client-side mentre il server non segnala nulla di sbagliato.
Lo scopo è impedire che una pagina su evil.example usi la tua sessione autenticata nel browser per leggere le risposte dell’API della tua banca. Senza questa regola, qualsiasi sito che visiti potrebbe inviare richieste autenticate a qualsiasi altro sito e leggerne le risposte.
Cosa permette CORS, e cosa non protegge
Cross-Origin Resource Sharing è un’eccezione opt-in a quella regola. Il server aggiunge header di risposta che indicano quali origin possono leggere le sue risposte, e il browser li rispetta invece di bloccare.
Il server non rifiuta niente. Gli header CORS sono istruzioni per il browser, non controllo degli accessi. Una richiesta curl, una chiamata backend-to-backend o un’app mobile nativa raggiungono lo stesso endpoint senza alcun controllo CORS. Se la tua API ha bisogno di protezione, quella si chiama autenticazione e autorizzazione.
Quando il browser invia una richiesta preflight CORS
Il browser divide ogni richiesta cross-origin in due categorie, e solo una delle due chiede il permesso prima.
Una richiesta è simple quando valgono tutte queste condizioni:
- Il metodo è
GET,HEADoPOST. - Gli unici header impostati sono quelli che il browser aggiunge automaticamente, più una lista corta consentita (
Accept,Accept-Language,Content-Language,Content-Type,Range). - Se
Content-Typeè impostato, il suo valore èapplication/x-www-form-urlencoded,multipart/form-dataotext/plain.
Una richiesta simple parte direttamente, e il browser controlla gli header di risposta per decidere se il tuo script riceve il body. Tutto il resto è preflighted: una PUT o una DELETE, un body JSON (Content-Type: application/json), o un header custom come Authorization o X-API-Key. Prima di inviare la richiesta vera, il browser manda una richiesta OPTIONS chiedendo al server cosa permette:
| |
Il server risponde con quello che consente. Solo se quella risposta copre la richiesta reale, il browser la invia:
| |
Access-Control-Max-Age indica per quanti secondi il browser può mettere in cache quella risposta e saltare il preflight alla prossima richiesta che corrisponde. Un’API JSON che invia un bearer token scatena un preflight quasi a ogni chiamata, quindi questo header risparmia un round trip ogni volta. I browser lo limitano: Chromium non lo mette in cache oltre 7200 secondi, qualunque valore tu invii.
Header di risposta CORS e cosa fa ciascuno
| Header | Cosa fa |
|---|---|
Access-Control-Allow-Origin | L’origin autorizzata a leggere la risposta: un’origin letterale, oppure * per qualsiasi origin. * viene rifiutato sulle richieste con credenziali. |
Access-Control-Allow-Methods | I metodi permessi su questa risorsa, restituiti nella risposta al preflight. |
Access-Control-Allow-Headers | Gli header di richiesta permessi, restituiti nella risposta al preflight. Authorization va elencato per nome; * non lo copre. |
Access-Control-Allow-Credentials | true permette al browser di esporre la risposta a una richiesta con credenziali (cookie, autenticazione HTTP). Richiede un’origin letterale, non *. |
Access-Control-Expose-Headers | Gli header di risposta, oltre alla safelist di default, che JavaScript può leggere. |
La regola sulle credenziali è quella che manda in confusione più persone. “Credenziali” qui significa cookie e autenticazione HTTP gestita dal browser, inviate quando chiami fetch(url, { credentials: 'include' }). Su una richiesta con credenziali, il server deve inviare Access-Control-Allow-Credentials: true e indicare l’origin esatta in Access-Control-Allow-Origin. Una risposta con credenziali e Access-Control-Allow-Origin: * viene rifiutata anche se tutti gli altri header sono corretti.
Un bearer token che imposti tu stesso in un header Authorization non è una credenziale in questo senso. È un header custom: scatena un preflight e deve comparire in Access-Control-Allow-Headers, ma non ha bisogno di Allow-Credentials.
Come risolvere gli errori CORS in Express
Il pacchetto cors imposta questi header al posto tuo.
| |
| |
In produzione hai spesso più di un frontend legittimo. Passa un array o una funzione invece di una singola stringa:
| |
Montato con app.use, cors() risponde al preflight OPTIONS per ogni rotta. Se lo monti solo su rotte specifiche che usano un metodo diverso da GET, aggiungi app.options("/api/users", cors()) così anche il preflight per quel percorso riceve una risposta.
Come abilitare CORS in ASP.NET Core
Registra una policy con nome in Program.cs e chiama UseCors tra UseRouting e UseAuthorization. L’ordine conta: un preflight non porta autenticazione, quindi se l’autorizzazione gira prima rifiuta la richiesta OPTIONS prima che CORS possa rispondere.
| |
WithOrigins accetta solo origin letterali, per lo stesso motivo dell’header grezzo. Combina AllowCredentials() con AllowAnyOrigin() e ASP.NET Core lancia una InvalidOperationException invece di inviare una risposta che il browser rifiuterebbe comunque. Per un controllo per-endpoint, metti [EnableCors("_allowFrontend")] su un controller o un’action e chiama app.UseCors() senza nome di policy: il middleware deve comunque essere nella pipeline perché l’attributo abbia effetto.
Come evitare CORS in sviluppo con un proxy Vite
Sul tuo laptop, la soluzione con meno sforzo è smettere di fare una richiesta cross-origin del tutto. Vite, webpack-dev-server e la maggior parte degli altri dev server possono fare da proxy verso il backend per un path, così il browser parla sempre con una sola origin:
| |
Una fetch verso /api/users ora va al dev server di Vite su http://localhost:5173, che la inoltra a http://localhost:4000 e ne inoltra la risposta. Il browser vede una sola origin, quindi non c’è nulla da bloccare per CORS.
Questa è una comodità di sviluppo. La versione in produzione della stessa idea è un reverse proxy che serve frontend e API da un’unica origin pubblica, il che elimina del tutto la necessità di header CORS su quell’API.
Workaround CORS da non usare
Ognuno di questi fa sparire il testo rosso per te e lascia il problema reale, un header che la tua API non invia, esattamente dov’era:
Access-Control-Allow-Origin: *su un endpoint che usa i cookie. Il browser rifiuta la risposta con credenziali, quindi questo non risolve nemmeno l’errore. Togli i cookie per far funzionare il wildcard e ti ritrovi un endpoint che risponde a qualsiasi sito su internet senza alcun controllo sull’origin.- Un’estensione del browser “CORS unblock”. Modifica le risposte dentro il tuo browser. L’errore sparisce per te e resta per ogni utente reale, incluso un collega che sta testando lo staging da un’altra macchina.
- Avviare Chrome con
--disable-web-security. Disattiva la same-origin policy per ogni tab di quell’istanza del browser, non solo per la tua app, finché resta aperta. Una tab dimenticata su un sito malevolo vanifica esattamente la protezione per cui esiste la policy.
Quale soluzione CORS usare
Se l’errore compare solo in locale, contro un backend su una porta diversa, usa il proxy del dev server e salta del tutto la configurazione CORS.
Se utenti reali chiameranno la tua API cross-origin da un dominio frontend separato o da un sito terzo, imposta Access-Control-Allow-Origin sulle origin esatte che intendi servire. Aggiungi Access-Control-Allow-Credentials solo se il browser invia cookie. Poi apri il tab Network di DevTools e controlla la richiesta OPTIONS prima di dare per scontato che abbia funzionato: un 2xx con Allow-Origin, Allow-Methods e Allow-Headers che coprono la tua richiesta reale significa che la richiesta successiva passerà.