Risolvere i problemi legati a CORS in Go con Gin
CORS (Cross-Origin Resource Sharing) è uno dei meccanismi di sicurezza più importanti del web moderno, ma è anche una delle fonti più frequenti di frustrazione per chi sviluppa API. Quando si lavora con Go e il framework Gin, la gestione di CORS richiede attenzione particolare a diversi aspetti: l'ordine dei middleware, la gestione del preflight, la corretta configurazione degli header consentiti e la registrazione dei middleware al livello giusto della gerarchia di routing. In questo articolo analizziamo i problemi più comuni e le strategie per risolverli in modo robusto.
Cosa è CORS e perché esiste
CORS è un meccanismo basato su header HTTP che consente al server di indicare al browser quali origin (combinazione di schema, host e porta) sono autorizzati ad accedere alle sue risorse. Senza CORS, una pagina servita da https://example.com non potrebbe leggere via JavaScript la risposta di una richiesta inviata a https://api.example.com, in virtù della Same-Origin Policy applicata dai browser.
È fondamentale capire che CORS è una politica applicata dal browser, non dal server. Il server si limita a dichiarare le proprie intenzioni tramite header come Access-Control-Allow-Origin; è il browser che decide se consegnare la risposta al codice JavaScript chiamante o bloccarla. Per questo motivo, una richiesta server-to-server (ad esempio da un altro backend in Go o Node.js) non è mai soggetta a restrizioni CORS.
Richieste semplici e richieste con preflight
Il browser distingue due tipi di richieste cross-origin: le richieste semplici e quelle con preflight. Le prime usano metodi come GET, HEAD o POST con un set ristretto di header standard e content type limitati a application/x-www-form-urlencoded, multipart/form-data o text/plain. Tutte le altre richieste innescano un preflight: il browser invia automaticamente una richiesta OPTIONS prima della richiesta vera, per verificare che il server accetti il metodo e gli header che il client intende usare.
Il preflight è il punto in cui la maggior parte dei problemi CORS si manifesta. Se il server non risponde correttamente all'OPTIONS, la richiesta principale non parte mai, e il browser riporta un errore generico che spesso confonde lo sviluppatore.
Un middleware CORS di base in Gin
Partiamo da un middleware CORS minimale che illustra la struttura essenziale. Questo esempio gestisce una whitelist di origin consentiti, imposta gli header necessari e risponde al preflight.
package middleware
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
func CorsMiddleware() gin.HandlerFunc {
originsString := "https://app.example.com,http://localhost:5173"
var allowedOrigins []string
if originsString != "" {
allowedOrigins = strings.Split(originsString, ",")
// Rimuove eventuali spazi attorno agli origin
for i, o := range allowedOrigins {
allowedOrigins[i] = strings.TrimSpace(o)
}
}
isOriginAllowed := func(origin string) bool {
for _, allowed := range allowedOrigins {
if origin == allowed {
return true
}
}
return false
}
return func(c *gin.Context) {
origin := c.Request.Header.Get("Origin")
allowed := isOriginAllowed(origin)
if allowed {
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers",
"Content-Type, Authorization, X-Requested-With, Accept, Origin, Cache-Control")
c.Writer.Header().Set("Access-Control-Allow-Methods",
"GET, POST, PUT, PATCH, DELETE, OPTIONS")
// Vary: Origin è essenziale per la cache
c.Writer.Header().Set("Vary", "Origin")
}
if c.Request.Method == "OPTIONS" {
if allowed {
c.AbortWithStatus(http.StatusNoContent)
} else {
c.AbortWithStatus(http.StatusForbidden)
}
return
}
c.Next()
}
}
Questo middleware contiene già diverse buone pratiche che vale la pena evidenziare. La funzione isOriginAllowed è definita una volta sola alla creazione del middleware, non ad ogni richiesta. La lista degli origin viene normalizzata con TrimSpace, prevenendo il problema classico in cui una variabile d'ambiente contiene spazi dopo le virgole e il confronto con l'uguaglianza fallisce silenziosamente. L'header Vary: Origin viene impostato per istruire eventuali cache intermedie (CDN, reverse proxy) a non servire la stessa risposta cached a origin diversi. Infine, il preflight viene rifiutato esplicitamente con 403 Forbidden quando l'origin non è autorizzato, producendo errori inequivocabili invece di mascherarli.
Il problema dell'ordine dei middleware
Uno degli errori più frequenti consiste nel registrare il middleware CORS dopo (o accanto a) altri middleware che possono respingere la richiesta prima che CORS abbia la possibilità di rispondere al preflight. Consideriamo il caso classico di un middleware di autenticazione che verifica un header custom:
func CheckTokenMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.Request.Header.Get("X-Auth-Token")
if token == "" {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
// Logica di validazione del token
c.Next()
}
}
Se questo middleware viene eseguito prima di CorsMiddleware, il preflight OPTIONS (che il browser invia senza header custom) verrà respinto con 401 e senza header CORS, facendo fallire l'intera negoziazione. La regola è semplice: CORS deve essere il primo middleware a girare, prima di qualsiasi altro controllo di autenticazione, autorizzazione o validazione.
Un secondo accorgimento consiste nel rendere il middleware di autenticazione consapevole del preflight, in modo che non venga eseguito su OPTIONS:
func CheckTokenMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// Le richieste preflight non portano header custom
if c.Request.Method == "OPTIONS" {
c.Next()
return
}
token := c.Request.Header.Get("X-Auth-Token")
if token == "" {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
c.Next()
}
}
Anche se in molti casi CorsMiddleware aborta già il flusso sull'OPTIONS e il middleware di auth non viene mai raggiunto, questa guardia aggiuntiva rende il codice più resiliente a eventuali refactoring dell'ordine dei middleware.
Il problema della registrazione per metodo
Un'altra fonte comune di malfunzionamenti riguarda la registrazione del middleware CORS solo su rotte specifiche, anziché a livello globale o di gruppo. Consideriamo il seguente codice problematico:
func AddRoutes(router *gin.RouterGroup) {
router.GET("/identity",
middleware.CorsMiddleware(),
middleware.CheckTokenMiddleware(),
controllers.Identity)
router.POST("/share",
middleware.CorsMiddleware(),
middleware.CheckTokenMiddleware(),
controllers.Share)
}
In Gin, i middleware passati come argomento a router.POST o router.GET vengono eseguiti soltanto per quel metodo HTTP su quel path. Quando il browser invia una richiesta OPTIONS /share per il preflight, Gin non trova alcun handler registrato per quel metodo e risponde con 404 o 405, senza mai eseguire il middleware CORS. Risultato: la risposta non contiene gli header Access-Control-Allow-*, il preflight fallisce, e la richiesta principale viene bloccata.
La soluzione corretta è registrare CorsMiddleware a livello del router group (o, ancora meglio, a livello globale dell'engine), in modo che intercetti qualsiasi metodo, incluso OPTIONS:
func AddRoutes(router *gin.RouterGroup) {
// CORS applicato all'intero gruppo: gestisce anche le OPTIONS
router.Use(middleware.CorsMiddleware())
router.GET("/identity",
middleware.CheckTokenMiddleware(),
controllers.Identity)
router.POST("/share",
middleware.CheckTokenMiddleware(),
controllers.Share)
}
Registrazione globale: la scelta migliore
Anche se la registrazione a livello di gruppo risolve il problema, l'approccio più pulito è registrare CORS a livello globale, sull'engine principale di Gin. CORS è una preoccupazione trasversale, non legata a una specifica risorsa, e merita lo stesso trattamento di logging, recovery e altri middleware infrastrutturali.
package main
import (
"example.com/api/middleware"
"example.com/api/routes"
"github.com/gin-gonic/gin"
)
func main() {
engine := gin.Default()
// CORS è il primo middleware in assoluto
engine.Use(middleware.CorsMiddleware())
apiGroup := engine.Group("/api")
routes.AddRoutes(apiGroup)
engine.Run(":3000")
}
Questo approccio offre tre vantaggi concreti. Primo, la policy CORS è dichiarata in un unico punto e si applica automaticamente a qualsiasi nuovo route group aggiunto in futuro. Secondo, anche le risposte 404 per endpoint inesistenti riceveranno header CORS corretti, evitando errori CORS confusi che mascherano il vero problema. Terzo, l'ordine di esecuzione è garantito: nessun altro middleware può girare prima di CORS e interferire con il preflight.
Configurazione tramite variabili d'ambiente
In un'applicazione di produzione, gli origin consentiti non vanno mai hardcoded nel sorgente. Una versione più realistica del middleware legge la lista da una variabile d'ambiente:
package middleware
import (
"net/http"
"os"
"strings"
"github.com/gin-gonic/gin"
)
func CorsMiddleware() gin.HandlerFunc {
originsString := os.Getenv("ALLOWED_ORIGINS")
var allowedOrigins []string
if originsString != "" {
allowedOrigins = strings.Split(originsString, ",")
for i, o := range allowedOrigins {
allowedOrigins[i] = strings.TrimSpace(o)
}
}
isOriginAllowed := func(origin string) bool {
for _, allowed := range allowedOrigins {
if origin == allowed {
return true
}
}
return false
}
return func(c *gin.Context) {
origin := c.Request.Header.Get("Origin")
allowed := isOriginAllowed(origin)
if allowed {
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers",
"Content-Type, Authorization, X-Requested-With, Accept, Origin, Cache-Control")
c.Writer.Header().Set("Access-Control-Allow-Methods",
"GET, POST, PUT, PATCH, DELETE, OPTIONS")
c.Writer.Header().Set("Access-Control-Max-Age", "86400")
c.Writer.Header().Set("Vary", "Origin")
}
if c.Request.Method == "OPTIONS" {
if allowed {
c.AbortWithStatus(http.StatusNoContent)
} else {
c.AbortWithStatus(http.StatusForbidden)
}
return
}
c.Next()
}
}
L'aggiunta di Access-Control-Max-Age con un valore di 86400 secondi (24 ore) istruisce il browser a memorizzare in cache l'esito del preflight per quel periodo, riducendo significativamente il numero di richieste OPTIONS per applicazioni che fanno molte chiamate API.
Credenziali e wildcard
Quando il client invia richieste con credenziali (cookie, header di autenticazione, certificati TLS client), in Axios questo si traduce in withCredentials: true; in fetch, in credentials: "include". Sul lato server è obbligatorio impostare Access-Control-Allow-Credentials: true, ma con un vincolo importante: Access-Control-Allow-Origin non può essere il wildcard *. Deve contenere l'origin esatto della richiesta.
Il middleware presentato sopra rispetta già questo vincolo, perché riflette dinamicamente l'origin della richiesta dopo averlo validato contro la whitelist. Questo pattern (echo dell'origin previa validazione) è il modo standard di supportare credenziali con CORS in modo sicuro.
Header custom e preflight
Qualsiasi header custom (cioè non incluso nella lista degli "header CORS-safelisted") fa scattare il preflight. Se il client invia un header come X-Auth-Token o App-Custom-Header, il browser invierà prima un OPTIONS contenente Access-Control-Request-Headers: x-auth-token, app-custom-header. Il server deve elencare esplicitamente questi header in Access-Control-Allow-Headers, altrimenti il preflight fallirà.
Un errore frequente consiste nel dimenticare di aggiornare la lista quando si introduce un nuovo header custom nel client. Se il backend conosce solo Authorization ma il frontend inizia a mandare X-Trace-Id, il browser bloccherà la richiesta. Mantieni l'elenco aggiornato e considera di documentarlo accanto al middleware.
Debug pratico: leggere il Network tab
Quando un errore CORS si manifesta, la prima cosa da fare è aprire i DevTools del browser e ispezionare la sezione Network. Cerca due richieste con lo stesso URL: una con metodo OPTIONS e una con il metodo applicativo (POST, PUT, ecc.). Se vedi solo l'OPTIONS, significa che il preflight è fallito.
Sulla richiesta OPTIONS verifica tre cose. Lo status code: deve essere 2xx, idealmente 204. I Request Headers: devono contenere Origin e Access-Control-Request-Method, più eventuali Access-Control-Request-Headers. I Response Headers: devono contenere Access-Control-Allow-Origin con il valore corretto, oltre agli altri header CORS configurati lato server.
Se nella sezione Request Headers vedi un avviso "Provisional headers are shown", la richiesta non è mai realmente partita dal browser; in genere è il sintomo di un preflight fallito a monte. In questo caso, ispeziona la richiesta OPTIONS separata per capire la causa radice.
Errori comuni e diagnosi rapida
Alcuni messaggi di errore tipici e le loro cause più frequenti. "No 'Access-Control-Allow-Origin' header is present on the requested resource": il preflight non riceve gli header CORS. Quasi sempre causato da middleware registrato per metodo invece che per gruppo, oppure da un altro middleware che aborta prima di CORS.
"The 'Access-Control-Allow-Origin' header has a value 'X' that is not equal to the supplied origin": l'origin richiesto non è in whitelist, oppure c'è un mismatch sottile (porta diversa, schema http vs https, slash finale).
"Credentials flag is 'true', but the 'Access-Control-Allow-Credentials' header is ''": il client invia credenziali ma il server non risponde con Access-Control-Allow-Credentials: true.
"Request header field X is not allowed by Access-Control-Allow-Headers in preflight response": il client invia un header custom non incluso nella lista Access-Control-Allow-Headers del server.
Una nota sulle librerie esistenti
Esiste una libreria ufficiale, gin-contrib/cors, che implementa un middleware CORS configurabile e ben testato. Per molte applicazioni è la scelta più sensata, perché copre casi limite (gestione di liste di origin con pattern, configurazione granulare per metodo, integrazione con il routing di Gin) che un'implementazione custom potrebbe trascurare.
package main
import (
"time"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
func main() {
engine := gin.Default()
engine.Use(cors.New(cors.Config{
AllowOrigins: []string{"https://app.example.com", "http://localhost:5173"},
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Authorization", "X-Requested-With"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))
engine.Run(":3000")
}
Tuttavia, scrivere un middleware custom resta utile in due scenari: quando si hanno requisiti specifici non coperti dalla libreria (ad esempio una validazione dinamica degli origin contro un database), e quando si vuole capire a fondo cosa accade sotto il cofano. La conoscenza dei meccanismi CORS è una di quelle competenze che ripaga ogni volta che qualcosa va storto in produzione.
Conclusioni
Risolvere i problemi CORS in Gin si riduce a tre principi fondamentali. Registrare il middleware CORS al livello giusto della gerarchia, preferibilmente globalmente sull'engine, in modo che intercetti tutti i metodi inclusi gli OPTIONS del preflight. Garantire che CORS sia il primo middleware a girare, prima di qualsiasi altro controllo che possa abortire la richiesta. Configurare correttamente gli header in funzione del caso d'uso reale: lista degli header consentiti aggiornata, gestione esplicita delle credenziali, riflessione dinamica dell'origin quando servono cookie o token.
Una volta interiorizzati questi principi, gli errori CORS smettono di essere un mistero e diventano problemi diagnosticabili in pochi minuti, leggendo le richieste OPTIONS nei DevTools e verificando la corrispondenza tra ciò che il browser chiede e ciò che il server risponde.