---
title: "CORS em Go: net/http, Chi e Gin sem Erros"
url: "https://golang.com.br/blog/cors-go-net-http-chi-gin/"
markdown_url: "https://golang.com.br/blog/cors-go-net-http-chi-gin.MD"
description: "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."
date: "2026-08-19"
author: "Golang Brasil"
---

# 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](/blog/autenticacao-autorizacao-go-apis/), [rate limiting](/blog/rate-limiting-go-api-producao/), 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:

```http
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
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.

```go
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:

```go
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:

```bash
go get github.com/rs/cors
```

Configuração:

```go
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:

```go
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](/blog/middleware-go-net-http-chi-gin-producao/) 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`.

```bash
go get github.com/gin-contrib/cors
```

```go
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](/blog/frameworks-http-go-gin-echo-fiber-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`:

```javascript
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.

```go
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](/blog/testes-tabela-go-guia-table-driven-tests/) 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:

```bash
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](/blog/configuracao-go-viper-variaveis-ambiente/) 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](/blog/context-timeout-cancelamento-go/), [graceful shutdown](/blog/graceful-shutdown-go-producao/), [health checks](/blog/health-checks-go-liveness-readiness-startup/) e [observabilidade com OpenTelemetry](/blog/go-opentelemetry-observabilidade-tracing-metricas/).

## 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

- [MDN: Cross-Origin Resource Sharing](https://developer.mozilla.org/pt-BR/docs/Web/HTTP/CORS)
- [Fetch Standard: CORS protocol](https://fetch.spec.whatwg.org/#http-cors-protocol)
- [Package github.com/rs/cors](https://pkg.go.dev/github.com/rs/cors)
- [Package github.com/gin-contrib/cors](https://pkg.go.dev/github.com/gin-contrib/cors)
- [Documentação de net/http](https://pkg.go.dev/net/http)
