Por que o navegador bloqueou seu fetch

Cole o erro do console. Além de traduzir, ele diz se a sua requisição dispara preflight e monta a resposta exata que o servidor precisa dar.

Roda no seu navegador · nada é enviado para servidor

Cole o erro do console

A mensagem vermelha inteira, do Access to fetch at… até o fim. É nela que está o motivo — a aba Network só mostra o sintoma.

Sua requisição

O que o servidor precisa responder

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.

Ou seja: um 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 preflightCom 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()
})
O 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.

Newsletter

Receba os artigos novos por e-mail

Sem spam. Só o aviso quando sai um tutorial ou artigo novo.