Resposta rápida: para configurar CORS em Go, coloque um middleware antes da autenticação, valide o header Origin contra uma lista explícita e responda ao preflight OPTIONS com os métodos e headers permitidos. Se houver cookies ou outra credencial enviada pelo navegador, não use Access-Control-Allow-Origin: *: devolva a origem autorizada, inclua Vary: Origin e habilite credenciais conscientemente. Em produção, prefira uma biblioteca consolidada, como github.com/rs/cors, a manter uma implementação incompleta por conta própria.
CORS costuma parecer um problema de Go, Chi, Gin ou do frontend, mas é um protocolo entre navegador e servidor HTTP. Entender essa divisão evita o ciclo clássico: adicionar *, continuar recebendo erro, liberar headers demais e descobrir depois que o preflight nem chegava ao handler correto.
Este guia mostra o fluxo completo com net/http, Chi e Gin, incluindo cookies, testes com httptest, diagnóstico e um checklist para produção.
O que é CORS e quando ele aparece
Cross-Origin Resource Sharing (CORS) é o mecanismo que permite ao servidor declarar quais origens podem ler suas respostas no navegador. Uma origem é a combinação de esquema, host e porta.
Por exemplo, estas origens são diferentes:
https://app.exemplo.com;https://api.exemplo.com;http://app.exemplo.com;https://app.exemplo.com:8443.
Se um frontend em https://app.exemplo.com chama uma API em https://api.exemplo.com, a chamada é cross-origin. O navegador envia o header Origin e decide se disponibiliza a resposta ao JavaScript com base nos headers CORS devolvidos pela API.
Isso também explica um ponto importante: CORS não é autenticação nem firewall. Um comando curl, um aplicativo móvel ou outro backend não depende dessa política do navegador. Sua API ainda precisa de autenticação e autorização, rate limiting, validação e logs.
Requisição simples vs preflight OPTIONS
Algumas chamadas são consideradas “simples” pelo navegador. Outras exigem uma consulta prévia chamada preflight.
Uma chamada com Authorization, método PUT, PATCH ou DELETE, Content-Type: application/json ou headers customizados normalmente dispara um OPTIONS antes da requisição real.
Exemplo de preflight:
OPTIONS /v1/pedidos HTTP/1.1
Origin: https://app.exemplo.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization,content-type
A API pode responder:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.exemplo.com
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 600
Vary: Origin
Depois disso, o navegador envia o POST. Se o OPTIONS receber 401, 404, redirect inesperado ou não trouxer os headers esperados, a requisição real pode nem acontecer.
Implementação manual com net/http
Uma implementação mínima deve fazer mais do que escrever Access-Control-Allow-Origin. Ela precisa validar a origem, tratar preflight e impedir que caches misturem respostas de origens diferentes.
package cors
import "net/http"
type Config struct {
Origins map[string]struct{}
}
func New(cfg Config) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if origin == "" {
next.ServeHTTP(w, r)
return
}
if _, allowed := cfg.Origins[origin]; !allowed {
if r.Method == http.MethodOptions {
http.Error(w, "origem não permitida", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
return
}
w.Header().Add("Vary", "Origin")
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Request-ID")
w.Header().Set("Access-Control-Max-Age", "600")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
}
Uso:
package main
import (
"log"
"net/http"
appcors "exemplo.com/api/internal/cors"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
corsMiddleware := appcors.New(appcors.Config{
Origins: map[string]struct{}{
"https://app.exemplo.com": {},
"http://localhost:5173": {},
},
})
log.Fatal(http.ListenAndServe(":8080", corsMiddleware(mux)))
}
Esse exemplo é didático, não uma biblioteca CORS completa. Uma implementação robusta também pode validar os valores de Access-Control-Request-Method e Access-Control-Request-Headers, controlar headers expostos e distinguir ambientes. Para a maioria das APIs, usar uma dependência pequena e testada é a decisão mais segura.
CORS com github.com/rs/cors
O package github.com/rs/cors funciona com qualquer http.Handler, portanto serve para net/http e Chi.
Instalação:
go get github.com/rs/cors
Configuração:
package main
import (
"log"
"net/http"
"github.com/rs/cors"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /v1/perfil", perfilHandler)
corsHandler := cors.New(cors.Options{
AllowedOrigins: []string{
"https://app.exemplo.com",
"http://localhost:5173",
},
AllowedMethods: []string{
http.MethodGet,
http.MethodPost,
http.MethodPut,
http.MethodPatch,
http.MethodDelete,
http.MethodOptions,
},
AllowedHeaders: []string{
"Authorization",
"Content-Type",
"X-Request-ID",
},
ExposedHeaders: []string{"X-Request-ID"},
AllowCredentials: true,
MaxAge: 600,
})
handler := corsHandler.Handler(mux)
log.Fatal(http.ListenAndServe(":8080", handler))
}
func perfilHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"nome":"Ana"}`))
}
Não copie AllowCredentials: true automaticamente. Habilite essa opção quando o browser realmente precisa enviar credenciais cross-origin, como cookies de sessão. Para uma API pública sem cookies, uma política mais restrita e simples costuma bastar.
CORS no Chi
Como o Chi usa http.Handler, a integração é direta:
package main
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/rs/cors"
)
func routes() http.Handler {
r := chi.NewRouter()
r.Use(cors.New(cors.Options{
AllowedOrigins: []string{"https://app.exemplo.com"},
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Authorization", "Content-Type"},
AllowCredentials: true,
MaxAge: 600,
}).Handler)
r.Route("/v1", func(r chi.Router) {
r.Get("/pedidos", listarPedidos)
r.Post("/pedidos", criarPedido)
})
return r
}
A ordem dos middlewares em Go importa. CORS deve conseguir responder ao OPTIONS antes de um middleware que exige token. Request ID e recovery podem vir antes; autenticação de rotas protegidas deve vir depois ou dentro do grupo correspondente.
CORS no Gin
No Gin, use um middleware compatível com gin.HandlerFunc, como github.com/gin-contrib/cors.
go get github.com/gin-contrib/cors
package main
import (
"time"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.New()
r.Use(gin.Recovery())
r.Use(cors.New(cors.Config{
AllowOrigins: []string{
"https://app.exemplo.com",
"http://localhost:5173",
},
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Authorization", "Content-Type", "X-Request-ID"},
ExposeHeaders: []string{"X-Request-ID"},
AllowCredentials: true,
MaxAge: 10 * time.Minute,
}))
r.GET("/v1/healthz", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
r.Run(":8080")
}
Mantenha a mesma política entre frameworks. Migrar de Gin para Chi não deveria alterar quais origens, métodos ou headers o produto permite; apenas a forma de registrar o middleware muda. Para comparar as opções, consulte frameworks HTTP em Go: Gin, Echo, Fiber e Chi.
Cookies, Authorization e credenciais
CORS com cookies reúne vários detalhes independentes:
- o servidor precisa autorizar a origem exata;
- a resposta precisa trazer
Access-Control-Allow-Credentials: true; - o frontend precisa enviar a chamada com credenciais;
- o cookie precisa ter atributos compatíveis com o cenário, como
Securee, em chamadas realmente cross-site,SameSite=None; - HTTPS deve estar configurado corretamente.
No frontend com fetch:
fetch("https://api.exemplo.com/v1/perfil", {
credentials: "include"
})
Um token no header Authorization não é cookie, mas o header costuma disparar preflight e precisa aparecer em Access-Control-Allow-Headers. Não confunda isso com a opção de credenciais do CORS: verifique o comportamento da biblioteca escolhida e teste o fluxo real no navegador.
CORS também não substitui proteção contra CSRF. Se o navegador envia cookies automaticamente, avalie SameSite, token CSRF, checagem de origem e reautenticação para ações sensíveis.
Como testar CORS com httptest
Teste tanto a origem permitida quanto a proibida. O caso negativo evita uma configuração que “funciona” liberando todo mundo.
package cors_test
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/rs/cors"
)
func TestPreflightPermitido(t *testing.T) {
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("o preflight não deveria chegar ao handler final")
})
h := cors.New(cors.Options{
AllowedOrigins: []string{"https://app.exemplo.com"},
AllowedMethods: []string{"GET", "POST", "OPTIONS"},
AllowedHeaders: []string{"Authorization", "Content-Type"},
}).Handler(next)
req := httptest.NewRequest(http.MethodOptions, "/v1/pedidos", nil)
req.Header.Set("Origin", "https://app.exemplo.com")
req.Header.Set("Access-Control-Request-Method", http.MethodPost)
req.Header.Set("Access-Control-Request-Headers", "authorization,content-type")
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusNoContent {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusNoContent)
}
if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "https://app.exemplo.com" {
t.Fatalf("allow origin = %q", got)
}
}
Adicione testes para:
- requisição real
GETcom origem permitida; - origem desconhecida sem
Access-Control-Allow-Origin; - método não autorizado;
- header customizado não autorizado;
Vary: Originquando a resposta depende da origem;- credenciais e headers expostos, se usados;
- middleware de autenticação não interceptando o preflight.
Use testes de tabela em Go para cobrir a matriz sem duplicar preparação.
Diagnóstico dos erros mais comuns
“No Access-Control-Allow-Origin header”
Confirme se a resposta veio da sua aplicação. Um proxy, CDN, gateway ou página de erro pode ter respondido antes do Go. Confira status, URL final e headers na aba Network do navegador.
Preflight com 401
CORS está depois da autenticação. Mova o middleware para fora do grupo protegido ou permita que OPTIONS seja tratado antes da validação do token.
Preflight com 404 ou 405
O roteador não reconheceu OPTIONS, ou o middleware não interceptou a chamada. Bibliotecas CORS normalmente tratam isso, desde que estejam registradas no nível correto.
Funciona no curl, mas não no navegador
Comportamento esperado: curl não aplica a política CORS. Reproduza o preflight manualmente para inspecionar a resposta:
curl -i -X OPTIONS 'https://api.exemplo.com/v1/pedidos' \
-H 'Origin: https://app.exemplo.com' \
-H 'Access-Control-Request-Method: POST' \
-H 'Access-Control-Request-Headers: authorization,content-type'
Funciona localmente, mas não em produção
Compare a origem byte a byte: esquema, porta e subdomínio. Revise também redirects HTTP→HTTPS, proxy reverso, configuração por variável de ambiente e cache. O guia de configuração em Go com Viper ajuda a separar allowlists de desenvolvimento e produção sem hardcode espalhado.
Checklist de CORS para produção
- Origens de produção estão em allowlist explícita.
-
localhostsó é permitido onde realmente necessário. - CORS roda antes de auth nas requisições preflight.
- Métodos permitidos refletem as rotas reais.
- Headers permitidos são os necessários, não uma lista copiada sem revisão.
-
Vary: Originestá presente quando a origem da resposta varia. -
AllowCredentialssó está ativo se o produto usa credenciais cross-origin. - Cookies têm
Secure,HttpOnlyeSameSiteadequados. - Origens negadas são cobertas por testes.
- Proxy, CDN e gateway preservam os headers.
- A API não trata CORS como substituto de autenticação ou autorização.
- Logs registram falhas úteis sem gravar tokens ou dados sensíveis.
Para uma API completa, combine CORS com timeouts e context, graceful shutdown, health checks e observabilidade com OpenTelemetry.
Perguntas frequentes
Como habilitar CORS em uma API Go?
Adicione um middleware que valide Origin contra uma lista explícita, escreva Access-Control-Allow-Origin somente para origens autorizadas, inclua Vary: Origin e responda ao preflight OPTIONS. Em produção, github.com/rs/cors é uma opção consolidada para net/http e Chi; no Gin, use um middleware compatível como gin-contrib/cors.
Posso usar Access-Control-Allow-Origin com asterisco?
Use * apenas em recursos realmente públicos e sem credenciais. Navegadores não aceitam wildcard com credenciais, e permitir qualquer origem pode expor dados que não deveriam ser lidos por scripts de sites arbitrários. Para aplicações autenticadas no navegador, use allowlist explícita.
Por que a requisição OPTIONS recebe 401 ou 404?
O preflight está chegando à autenticação ou ao roteador antes de ser tratado. Posicione CORS antes da autenticação e responda OPTIONS com 204 quando origem, método e headers forem permitidos. Verifique também se o middleware foi instalado no router raiz, não apenas em uma rota interna.
CORS protege a API contra chamadas de outros servidores?
Não. CORS é aplicado pelo navegador. Outros servidores, CLIs e aplicativos podem chamar a API. Proteção real exige autenticação, autorização, rate limiting e validação no backend.
Como testar CORS em Go?
Use httptest para criar chamadas simples e preflight com os headers Origin, Access-Control-Request-Method e Access-Control-Request-Headers. Valide os headers positivos e também sua ausência para origens proibidas. Complete com um teste no navegador porque cookies, redirects e políticas do ambiente podem alterar o fluxo.
Conclusão
Configurar CORS em Go não é “liberar um header”: é declarar uma política precisa para o navegador. A solução segura valida origens, trata preflight antes da autenticação, permite apenas métodos e headers necessários e testa explicitamente o caso proibido.
Se a API usa net/http ou Chi, github.com/rs/cors evita manter detalhes delicados manualmente. No Gin, use gin-contrib/cors com a mesma política. Comece restritivo, observe os erros reais e amplie somente o necessário — nunca troque uma allowlist por * apenas para fazer o console parar de reclamar.