Três verdades que mudam tudo
1. Quem bloqueia é o navegador, não o servidor
CORS é uma regra que o navegador aplica, e só ele. É por
isso que o mesmo endereço funciona no curl, no Postman e no
Insomnia — nenhum deles implementa CORS. Ver a requisição passar por ali
não é prova de que o servidor está certo; é prova de que
a ferramenta não fiscaliza.
2. A requisição quase sempre chegou no servidor
Este é o detalhe que assusta quando cai a ficha: numa requisição simples, o navegador envia, o servidor processa, e só então o navegador olha os cabeçalhos da resposta e decide esconder o resultado do seu JavaScript.
POST pode criar o registro no banco
e ainda assim mostrar erro de CORS na tela. Se você já viu um formulário
“que falhou” duplicar dados, provavelmente foi isso.
3. O erro do console não é a resposta do servidor
O navegador esconde a resposta do JavaScript, então o seu
catch recebe um TypeError: Failed to fetch sem
detalhe nenhum. O motivo de verdade só aparece na mensagem vermelha do
console — por isso ela é o que esta ferramenta lê.
Preflight: por que aparece uma requisição OPTIONS que você não fez
Antes de certas requisições, o navegador manda um OPTIONS
perguntando “posso?”. Se a resposta não autorizar, a requisição de verdade
nunca sai.
A regra é ao contrário do que a intuição diz: preflight é o padrão, e
escapam dele apenas as requisições simples — as que um formulário
HTML comum já conseguia fazer antes do fetch existir:
| Sem preflight | Com preflight | |
|---|---|---|
| Método | GET, HEAD, POST | PUT, PATCH, DELETE… |
| Content-Type | x-www-form-urlencoded, multipart/form-data, text/plain | application/json |
| Cabeçalhos | Accept, Accept-Language, Content-Language, Range | Authorization, X-qualquer-coisa |
Repare no que isso significa na prática: quase toda API moderna
dispara preflight, porque manda JSON e usa
Authorization. Cada uma dessas duas coisas, sozinha, já basta.
O preflight tem duas exigências que derrubam a maioria
Precisa responder 2xx. É aqui que morre a maior parte:
o middleware de autenticação intercepta o OPTIONS, não acha
token — porque o navegador não manda credencial no preflight — e
devolve 401. O console diz “does not have HTTP ok status”, e a pessoa vai
procurar erro de CORS quando o problema é a ordem dos middlewares.
// Express: CORS ANTES da autenticação, sempre
app.use(cors(opcoes))
app.use(autenticacao) // se inverter, o preflight leva 401
Não pode redirecionar. Um 301 de http para
https, ou de com-barra para sem-barra, mata o preflight — o
navegador não segue redirecionamento nessa etapa.
A armadilha do * com credencial
Esta é a mais confusa, e a mensagem do navegador é literal:
The value of the 'Access-Control-Allow-Origin' header must not be
the wildcard '*' when the request's credentials mode is 'include'
Faz sentido quando você percebe o que o * quer dizer:
“qualquer site pode ler minha resposta”. Somando cookie a isso,
qualquer site poderia fazer requisição autenticada como você e ler o
resultado — que é exatamente o ataque que o CORS existe para impedir.
Com credencial, o servidor precisa ecoar a origem específica e assumir a responsabilidade de conferir quem é:
const PERMITIDAS = new Set([
'https://app.exemplo.com',
'http://localhost:3000'
])
app.use((req, res, next) => {
const origem = req.headers.origin
if (PERMITIDAS.has(origem)) {
res.setHeader('Access-Control-Allow-Origin', origem)
res.setHeader('Access-Control-Allow-Credentials', 'true')
res.setHeader('Vary', 'Origin') // ← não esqueça esta linha
}
next()
})
Vary: Origin não é detalhe. Sem ele,
qualquer cache no caminho — CDN, proxy, o próprio navegador — guarda a
resposta com a origem de outra pessoa e serve para todo mundo. O
resultado é CORS que funciona para um usuário e quebra para o vizinho, de
forma intermitente e quase impossível de reproduzir.
Ler cabeçalho da resposta pelo JavaScript
Outra que parece bug: o servidor mandou X-Total-Paginas, você
vê na aba Network, e res.headers.get('X-Total-Paginas')
devolve null. Não é bug — por padrão o JS só enxerga um punhado
de cabeçalhos. O resto precisa ser liberado explicitamente:
Access-Control-Expose-Headers: X-Total-Paginas, X-Pagina-Atual
Quando “erro de CORS” não é CORS
Se a mensagem for Failed to fetch ou “CORS request did not
succeed” sem citar nenhum cabeçalho, provavelmente
nem houve resposta para inspecionar. Verifique, nesta ordem: o servidor
está no ar; a porta está certa; o certificado é válido (página HTTPS não
chama endereço HTTP); e não é a extensão de bloqueio do navegador.
# o servidor responde e manda os cabeçalhos?
curl -sI -X OPTIONS https://api.exemplo.com/pedidos \
-H "Origin: http://localhost:3000" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: authorization,content-type"
Se esse comando não devolver Access-Control-Allow-* na
resposta, o problema é do servidor — e agora você tem prova.
Se a API está atrás de um proxy reverso, os cabeçalhos podem estar sendo perdidos ou duplicados no caminho: isso está no artigo de proxy reverso, e o diagnóstico de Nginx 502 cobre o resto da camada.