Il messaggio suona più o meno così, e la metà che conta è la fine:

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.

Leggilo come una frase su chi ha rifiutato. Il server ha risposto. La tua richiesta ha lasciato il browser, ha raggiunto l’altra macchina ed è tornata con una risposta. Il browser poi si è rifiutato di consegnare quella risposta al tuo JavaScript, perché la risposta non diceva che la tua origine avesse il permesso di leggerla.

Questo unico fatto esclude quasi tutto quello che si prova per primo. Sulla rete non c’è niente di rotto. Il server non è giù. Non c’è nessun firewall di mezzo. La risposta sta lì nel browser, e il browser non la passa.

Perché il browser lo fa

Senza, qualsiasi pagina tu visiti potrebbe fare di nascosto richieste alla tua banca, alla tua webmail o agli strumenti interni della tua azienda usando i cookie già presenti nel browser, e leggerne le risposte.

La regola quindi è: JavaScript può inviare una richiesta verso un’altra origine, ma può leggere la risposta solo se il server che risponde dice che quell’origine ne ha il diritto. Il permesso deve arrivare dal server a cui si chiede, perché è l’unico a sapere chi dovrebbe leggere.

Due conseguenze che sorprendono:

  • Vale solo per il JavaScript del browser. La stessa richiesta da curl, da Postman o dal tuo backend funziona, perché nessuno di questi è un browser che custodisce la sessione di qualcuno. Una richiesta che passa in Postman e fallisce nella pagina non è la prova che il server sia rotto.
  • Disattivarlo nel tuo browser non risolve niente. Un flag o un’estensione che spegne il controllo fa sparire l’errore su una macchina mentre ogni visitatore continua a riceverlo. È un modo per confermare la diagnosi, non una soluzione, e usare un browser così tutti i giorni è davvero una pessima idea.

Leggere quale regola è saltata

La fine del messaggio nomina la regola, e ce ne sono tre che incontrerai.

No ‘Access-Control-Allow-Origin’ header is present. (Non è presente alcuna intestazione ‘Access-Control-Allow-Origin’.) Il caso semplice. Il server non ha inviato nulla sui permessi, quindi il browser presume che non ce ne siano.

Response to preflight request doesn’t pass access control check. (La risposta alla richiesta di preflight non supera il controllo di accesso.) Il fallimento è avvenuto prima ancora che la tua richiesta partisse. Tutto ciò che va oltre una richiesta semplice, un PUT o un DELETE, un’intestazione personalizzata come Authorization, un content type JSON, fa sì che il browser invii prima una richiesta OPTIONS, che chiede se quella vera è permessa. Se quell’OPTIONS restituisce un 404, un 500, un redirect o un 200 senza le intestazioni giuste, la richiesta vera non avviene mai.

*The value of ‘Access-Control-Allow-Origin’ must not be the wildcard ‘’ when credentials mode is ‘include’.** (Il valore non può essere il carattere jolly ‘*’ quando si inviano le credenziali.) Stai inviando i cookie, e * non è abbastanza specifico per questo. Il server deve nominare esattamente la tua origine e aggiungere Access-Control-Allow-Credentials: true.

Dove va la correzione

Sul server che possiede la risorsa, in ogni caso. Non nella tua pagina, non nel browser, non in un proxy messo davanti al tuo front end.

Se l’API è tua, vuol dire restituire Access-Control-Allow-Origin con l’origine che vuoi permettere e, per i preflight, rispondere a OPTIONS con i metodi e le intestazioni consentiti e uno stato 2xx. Se l’API è di qualcun altro e non consente l’accesso dal browser, allora l’accesso dal browser non è previsto: chiamala dal tuo backend e lascia che sia la tua pagina a parlare con quello.

Session Replay

Estensione gratuita per Chrome. Un clic sulla pagina che si comporta male cattura lo screenshot, la console e il registro di rete, e ti restituisce un link da incollare nel ticket.

Installa l'estensione

Su localhost, dove quasi tutti lo incontrano

http://localhost:3000 e http://localhost:8080 sono origini diverse, quindi un front end su una porta che chiama un’API su un’altra fa una richiesta cross-origin e si prende il trattamento completo.

La risposta giusta in sviluppo è un proxy: il tuo dev server inoltra /api all’API, così il browser vede una sola origine e la questione non si pone. Ogni strumento front end moderno ce l’ha di serie e costa una riga di configurazione. La risposta sbagliata è avviare Chrome con la sicurezza web disattivata, perché poi il codice funziona solo per chi ha fatto lo stesso.

Che cosa vede il tuo codice

Niente di utile, ed è questa la parte che rende confuso il debug.

Una risposta bloccata non arriva come un errore che puoi ispezionare. fetch() viene rifiutato con il generico TypeError: Failed to fetch, senza stato e senza corpo, perché lasciare che la pagina legga perché è stata bloccata rivelerebbe proprio l’informazione che la regola esiste per proteggere.

Quindi il messaggio CORS nella console non è un errore che il tuo codice ha catturato. È il browser che dice a te, che sviluppi, che cosa ha fatto. Il tuo codice non può vederlo, non può registrarlo e non può segnalarlo.

Vale la pena saperlo quando la segnalazione arriva da qualcun altro. Un utente che incappa in un fallimento CORS vede una funzione che in silenzio non fa niente, il tuo tracciamento degli errori registra un Failed to fetch senza dettagli, e la frase che l’avrebbe spiegato è stata stampata in una console che nessuno ha conservato.

Trovarlo in una segnalazione

Due posti, e li vuoi entrambi.

La console porta il messaggio CORS con l’origine, l’URL e la regola saltata. Il pannello di rete mostra la richiesta stessa, e, per un fallimento di preflight, la richiesta OPTIONS che sta sopra a quella vera, con il suo stato. Apri le sue intestazioni di risposta: quello che lì manca è tutta la diagnosi.

Se la richiesta non compare affatto nel pannello di rete, il browser ha rifiutato prima di inviarla, il che vuol dire contenuto misto o un’estensione, non CORS.

In breve

  • Il server ha risposto; il browser si è rifiutato di far leggere la risposta al tuo codice.
  • Vale solo per il JavaScript del browser, quindi curl e Postman non dimostrano niente.
  • La fine del messaggio dice quale regola è saltata. Leggi quella, non l’inizio.
  • I fallimenti di preflight avvengono prima della tua richiesta, su un OPTIONS che il browser ha inviato per te.
  • La correzione spetta al server che risponde. Disattivare il controllo in locale è una diagnosi, non una soluzione.
  • Su localhost, usa un proxy di sviluppo così che ci sia una sola origine.