- Diagnóstico rápido: é mesmo CORS?
- Por que o browser bloqueia
- O preflight request
- Headers CORS essenciais
- Correção por framework
- Erros comuns com mensagens do console
- Armadilhas de segurança
- CORS em produção
- FAQ
Você está olhando para o console. Tem uma mensagem vermelha. Você testou a API no Postman e funcionou. O endpoint existe, o servidor está rodando, o código está certo — e mesmo assim o browser recusa.
Esse é o momento em que a maioria das pessoas começa a tentar coisas aleatórias. Adiciona um header aqui, muda uma configuração ali, instala um pacote sem entender o que faz.
Este artigo elimina o chute. Você vai entender o que está acontecendo, diagnosticar pelo console e corrigir — de forma definitiva, no framework que está usando.
Diagnóstico rápido: é mesmo CORS?
Antes de qualquer coisa, confirme que o problema é CORS e não outra coisa.
Abra o DevTools, vá na aba Network, localize a requisição com erro e verifique:
É CORS se:
- A requisição tem status
(blocked)ou aparece com erro de rede sem código HTTP - O console mostra
Cross-Origin Request Blockedouhas been blocked by CORS policy - Existe uma requisição
OPTIONSna lista (o preflight) Não é CORS se: - A resposta tem status
401ou403— é autenticação/autorização, não CORS - A resposta tem status
5xx— é erro no servidor - A requisição nem aparece no Network — pode ser erro de código antes do fetch Confirmado o CORS? Siga adiante.
Por que o browser bloqueia (e o Postman não)
O browser implementa a Same-Origin Policy (SOP): por padrão, scripts rodando em http://app.com não podem fazer requisições para http://outraapi.com. Isso não é limitação técnica — é proteção deliberada.
Origem é a combinação exata de protocolo + domínio + porta. Qualquer diferença cria uma origem diferente:
| Origem da página | URL da requisição | Cross-origin? |
|---|---|---|
http://site.com | https://site.com/api | Sim — protocolo diferente |
http://site.com | http://site.com:3001/api | Sim — porta diferente |
http://site.com | http://api.site.com/dados | Sim — subdomínio diferente |
http://site.com | http://site.com/api | Não — mesma origem |
O Postman, o curl, o Insomnia — nenhum deles é um browser. Não implementam SOP. Quando você testa a API lá e "funciona", o servidor está respondendo corretamente. O problema está na camada de segurança que só existe no contexto do browser — onde há cookies, storage e credenciais do usuário em jogo.
Por que a SOP existe: imagine que você está logado no banco.com. Sem SOP, qualquer site malicioso poderia fazer uma requisição para banco.com/transferencia usando seus cookies de sessão, sem você saber. A SOP impede isso.
CORS é o mecanismo que permite ao servidor abrir exceções controladas a essa proteção. Sem CORS configurado, o browser descarta a resposta — mesmo que ela tenha chegado com status 200.
O preflight request: a requisição que você não pediu
Quando o browser detecta uma requisição cross-origin "não simples", ele envia uma requisição OPTIONS primeiro — o preflight — para verificar se o servidor autoriza a operação.
O que define uma requisição "simples":
Uma requisição é simples se usa GET, HEAD ou POST com Content-Type limitado a text/plain, multipart/form-data ou application/x-www-form-urlencoded, sem headers customizados.
Na prática: toda requisição com Content-Type: application/json ou com Authorization dispara um preflight. Ou seja, toda requisição de API moderna.
Como o preflight funciona:
1. Seu código executa: fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' } })
2. Browser envia PRIMEIRO:
OPTIONS /api/users HTTP/1.1
Origin: http://localhost:3000
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
3. Servidor deve responder:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
4. Só então o browser envia o POST real.
O efeito colateral que confunde todo mundo: se o preflight falhar, o browser cancela a requisição real antes de enviá-la. No console do browser você vê o erro. No terminal do servidor, silêncio — porque o POST nunca chegou.
Isso explica por que "o servidor parece estar ignorando a requisição".
Headers CORS essenciais
Esses são os headers que o servidor precisa enviar para liberar o acesso. Cada um tem uma função específica.
Headers de resposta do servidor:
| Header | Função | Exemplo |
|---|---|---|
Access-Control-Allow-Origin | Qual origem pode acessar | https://meuapp.com ou * |
Access-Control-Allow-Methods | Métodos HTTP permitidos | GET, POST, PUT, DELETE |
Access-Control-Allow-Headers | Headers que o client pode enviar | Content-Type, Authorization |
Access-Control-Allow-Credentials | Permite cookies e credenciais | true |
Access-Control-Max-Age | Cache do preflight em segundos | 86400 (24h) |
Access-Control-Expose-Headers | Headers da resposta visíveis ao JS | X-Total-Count |
A regra que quebra muita implementação:
Access-Control-Allow-Origin: * e Access-Control-Allow-Credentials: true não coexistem. O browser rejeita explicitamente essa combinação.
Se você precisa enviar cookies ou usar Authorization:
// ❌ Vai falhar — wildcard com credentials é inválido
fetch('https://api.exemplo.com/data', { credentials: 'include' })
// Servidor responde: Access-Control-Allow-Origin: *
// O servidor precisa especificar a origem exata:
// Access-Control-Allow-Origin: https://meuapp.com
// Access-Control-Allow-Credentials: true
Sobre o Access-Control-Max-Age:
Sem esse header, o browser faz um preflight a cada requisição. Com 86400, ele cacheia por 24 horas. Em APIs com volume, isso reduz latência e requisições desnecessárias de forma significativa.
Correção por framework
Node.js com Express
O erro mais comum: registrar o middleware de CORS depois das rotas.
const express = require('express')
const cors = require('cors')
const app = express()
// ❌ Errado — CORS depois das rotas não funciona para as rotas já definidas
app.get('/api/users', handler)
app.use(cors())
// Correto — CORS antes de qualquer rota
app.use(cors({
origin: ['https://meuapp.com', 'http://localhost:3000'],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400
}))
// Tratar preflight explicitamente para todas as rotas
app.options('*', cors())
Se você precisa de lógica de validação de origem dinâmica:
const allowedOrigins = ['https://meuapp.com', 'https://admin.meuapp.com']
app.use(cors({
origin: (origin, callback) => {
// Permite requisições sem origin (Postman, curl, mobile apps)
if (!origin) return callback(null, true)
if (allowedOrigins.includes(origin)) {
callback(null, true)
} else {
callback(new Error(`Origem não permitida: ${origin}`))
}
},
credentials: true
}))
Next.js — App Router
Tratar no middleware.ts global é mais limpo do que repetir headers em cada rota:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const ALLOWED_ORIGINS = [
'https://meuapp.com',
'http://localhost:3000',
]
export function middleware(request: NextRequest) {
const origin = request.headers.get('origin') ?? ''
const isAllowed = ALLOWED_ORIGINS.includes(origin)
// Resposta para preflight
if (request.method === 'OPTIONS') {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': isAllowed ? origin : '',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
},
})
}
const response = NextResponse.next()
if (isAllowed) {
response.headers.set('Access-Control-Allow-Origin', origin)
response.headers.set('Access-Control-Allow-Credentials', 'true')
}
return response
}
export const config = {
matcher: '/api/:path*',
}
Para rotas que precisam de controle individual, implemente o handler OPTIONS diretamente:
// app/api/usuarios/route.ts
const corsHeaders = {
'Access-Control-Allow-Origin': 'https://meuapp.com',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}
export async function OPTIONS() {
return new Response(null, { status: 204, headers: corsHeaders })
}
export async function GET() {
const dados = await buscarUsuarios()
return Response.json(dados, { headers: corsHeaders })
}
Vite (proxy em desenvolvimento)
Quando você não controla o backend e precisa resolver localmente:
// vite.config.ts
import { defineConfig } from 'vite'
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'https://api-externa.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
configure: (proxy) => {
proxy.on('error', (err) => {
console.log('Proxy error:', err)
})
}
}
}
}
})
O browser faz a requisição para localhost (mesma origem). O Vite redireciona para a API externa no lado do servidor. Sem CORS, porque a política só se aplica ao browser.
Isso só funciona em desenvolvimento. Em produção você precisa de CORS real ou de um proxy real (nginx, Cloudflare Workers, seu próprio backend de proxy).
Erros comuns com mensagens exatas do console
"No 'Access-Control-Allow-Origin' header is present"
Access to fetch at 'https://api.exemplo.com' from origin 'http://localhost:3000'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.
Causa: O servidor não tem middleware CORS configurado, ou o middleware não está sendo aplicado a essa rota específica.
Verifique: o middleware está registrado antes das rotas? A rota está dentro do matcher do middleware?
"Response to preflight request doesn't pass access control check"
Response to preflight request doesn't pass access control check:
It does not have HTTP ok status.
Causa: A rota OPTIONS está retornando 404 ou 405. Muitos frameworks não tratam OPTIONS por padrão.
Solução: Adicione tratamento explícito para OPTIONS antes das demais rotas.
"Request header field X is not allowed"
Request header field Authorization is not allowed by
Access-Control-Allow-Headers in preflight response.
Causa: O servidor lista Content-Type no Access-Control-Allow-Headers, mas não lista Authorization (ou outro header que você está enviando).
Solução: Adicione o header faltante no allowedHeaders. A mensagem de erro informa exatamente qual header está ausente — leia com atenção antes de tentar qualquer outra coisa.
"The value of the 'Access-Control-Allow-Origin' header must not be the wildcard"
The value of the 'Access-Control-Allow-Origin' header in the response must not
be the wildcard '*' when the request's credentials mode is 'include'.
Causa: Você usa credentials: 'include' na requisição, mas o servidor responde com Access-Control-Allow-Origin: *.
Solução: O servidor precisa responder com a origem exata (https://meuapp.com) e incluir Access-Control-Allow-Credentials: true.
Armadilhas de segurança: o que nunca fazer
CORS mal configurado não é apenas um problema funcional — é uma vulnerabilidade.
1. Refletir o Origin sem validação
// ❌ Vulnerável — aceita qualquer origem sem verificar
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', req.headers.origin) // NUNCA faça isso
res.header('Access-Control-Allow-Credentials', 'true')
next()
})
Isso significa que http://site-malicioso.com pode fazer requisições autenticadas para sua API usando as credenciais do usuário. É o ataque que a SOP foi criada para impedir.
2. Wildcard em rotas autenticadas
// ❌ Se a rota requer token, wildcard anula a proteção de origem
app.use(cors({ origin: '*' }))
app.get('/api/dados-sensiveis', authMiddleware, handler)
Use wildcard apenas para APIs verdadeiramente públicas, sem autenticação e sem dados de usuário.
3. localhost em produção
// ❌ Não deixe origens de dev no servidor de produção
origin: ['https://meuapp.com', 'http://localhost:3000'] // em produção
Mantenha a lista de origens separada por ambiente, via variáveis de ambiente.
Como diagnosticar se sua API está vulnerável:
# Testa se o servidor reflete o Origin sem validar
curl -H "Origin: https://site-atacante.com" \
-H "Cookie: sessao=token_real" \
https://suaapi.com/dados \
-v 2>&1 | grep "access-control-allow-origin"
# Se retornar: access-control-allow-origin: https://site-atacante.com
# Você tem um problema.
CORS em produção: cenários que o guia básico não cobre
API atrás de CDN (Cloudflare, CloudFront)
CDNs fazem cache dos headers de resposta. Se a sua API retorna Access-Control-Allow-Origin: https://meuapp.com, a CDN pode servir essa resposta em cache para uma origem diferente.
Solução: inclua Vary: Origin nos headers de resposta. Isso instrui a CDN a tratar respostas de origens diferentes como entradas de cache separadas.
res.header('Vary', 'Origin')
res.header('Access-Control-Allow-Origin', allowedOrigin)
Múltiplos subdomínios
Se você precisa liberar app.meusite.com, admin.meusite.com e api.meusite.com, não use wildcard. Use validação de lista ou regex:
const ALLOWED = /^https:\/\/(app|admin|api)\.meusite\.com$/
origin: (origin, callback) => {
if (!origin || ALLOWED.test(origin)) {
callback(null, true)
} else {
callback(new Error('Origem bloqueada'))
}
}
Teste de preflight em produção antes do deploy
# Teste manual do preflight com curl
curl -X OPTIONS https://suaapi.com/endpoint \
-H "Origin: https://meuapp.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type, Authorization" \
-v 2>&1 | grep -i "access-control"
Se os headers aparecerem na saída, o servidor está configurado corretamente. Se não aparecerem, você sabe exatamente onde investigar.
FAQ
CORS funciona em localhost mas quebra em produção. Por quê?
Em desenvolvimento, o servidor está liberando http://localhost:3000. Em produção, a origem mudou para https://meuapp.com — diferente em protocolo, domínio e porta. A lista de origens permitidas no servidor de produção precisa incluir a origem de produção, não a de dev.
Posso resolver CORS só pelo frontend?
Não. O browser impõe a política. O único contorno sem mexer no backend é usar um proxy server-side que faça a requisição por você (como o Vite em dev). Extensões de browser que "desativam CORS" funcionam apenas para quem instalou — não para seus usuários.
O servidor retorna os headers CORS mas o erro persiste. O que verificar?
- Abra a aba Network e clique na requisição
OPTIONS(não na requisição principal) - Inspecione a aba "Response Headers"
- Compare o valor exato de
Access-Control-Allow-Origincom a origem do browser na barra de endereço — protocolo, domínio e porta incluídos - Verifique se
Access-Control-Allow-Headerslista todos os headers que sua requisição está enviando - Se usar
credentials: 'include', confirme que o servidor não está retornando wildcard
Por que o curl funciona mas o browser bloqueia?
Porque curl não implementa Same-Origin Policy. Ele não é um browser. Fazer uma requisição funcionar no curl confirma que o servidor está respondendo — não que o CORS está configurado.
CORS protege minha API de ataques externos?
Parcialmente. CORS impede que browsers façam requisições cross-origin não autorizadas, mas não protege contra: requisições diretas via curl/Postman, ataques de backend para backend, ou usuários mal-intencionados que modificam o browser. CORS é proteção de browser para usuários legítimos — não é substituto de autenticação.
Checklist de verificação
Antes de qualquer deploy que envolva CORS:
- Middleware CORS registrado antes de todas as rotas
- Rota
OPTIONStratada explicitamente - Lista de origens definida por variável de ambiente (não hardcoded)
- Nenhuma origem de
localhostno ambiente de produção -
Access-Control-Allow-Headersinclui todos os headers que o client envia - Se usa credenciais: origem específica +
Allow-Credentials: true(sem wildcard) -
Vary: Originconfigurado se a API está atrás de CDN - Preflight testado com
curl -X OPTIONSantes do deploy