← Voltar para o blog

CORS em Go: net/http, Chi e Gin sem Erros

Configure CORS em Go com net/http, Chi e Gin: preflight OPTIONS, cookies, origens permitidas, headers, testes práticos e erros comuns em produção.

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:

  1. o servidor precisa autorizar a origem exata;
  2. a resposta precisa trazer Access-Control-Allow-Credentials: true;
  3. o frontend precisa enviar a chamada com credenciais;
  4. o cookie precisa ter atributos compatíveis com o cenário, como Secure e, em chamadas realmente cross-site, SameSite=None;
  5. 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 GET com origem permitida;
  • origem desconhecida sem Access-Control-Allow-Origin;
  • método não autorizado;
  • header customizado não autorizado;
  • Vary: Origin quando 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.
  • localhost só é 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: Origin está presente quando a origem da resposta varia.
  • AllowCredentials só está ativo se o produto usa credenciais cross-origin.
  • Cookies têm Secure, HttpOnly e SameSite adequados.
  • 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.

Referências