Você abriu o DevTools, viu Cross-Origin Request Blocked e foi direto ao Google. Alguém sugeriu jogar Access-Control-Allow-Origin: * no servidor. Funcionou — mas você não sabe por quê, e na próxima semana o problema voltou em outra forma.
Esse é o ciclo mais comum com CORS no desenvolvimento web. Este guia vai quebrar esse ciclo de vez: você vai entender o mecanismo real, os erros que todo dev comete e como configurar corretamente em qualquer stack — Express, Next.js, Vite e Nginx.
Índice
- O que é CORS (e o que ele não é)
- Como o mecanismo CORS funciona por dentro
- Os 4 erros mais comuns com CORS
- Como configurar CORS corretamente
- Referência rápida de headers CORS
- Checklist de configuração CORS
- FAQ — Perguntas frequentes
O que é CORS (e o que ele não é)
CORS — Cross-Origin Resource Sharing — é uma política de segurança implementada pelo navegador, não pelo servidor. Isso muda tudo na forma de debugar e resolver.
Quando seu frontend em http://localhost:3000 tenta buscar dados de http://localhost:8080, o navegador detecta origens diferentes e entra em modo de proteção. Ele não é opcional, não é um bug e não pode ser desativado no cliente.
Uma origem é composta por três partes:
| Componente | Exemplo A | Exemplo B | Mesma origem? |
|---|---|---|---|
| Protocolo | http | https | ❌ |
| Domínio | api.meusite.com | meusite.com | ❌ |
| Porta | 3000 | 8080 | ❌ |
Qualquer diferença em qualquer um desses três elementos cria origens distintas — e o CORS entra em ação.
Por que
curle Postman nunca têm esse problema? Porque são ferramentas que não implementam a política Same-Origin. CORS existe para proteger o usuário dentro do navegador, não para proteger o servidor. Ferramentas de linha de comando não têm esse contexto de segurança.
Como o mecanismo CORS funciona por dentro
Requisições simples
Chamadas GET e POST com Content-Type: application/x-www-form-urlencoded ou text/plain e apenas headers padrão são consideradas "simples" pela especificação. O navegador as envia diretamente e verifica os headers da resposta.
Se o servidor não incluir Access-Control-Allow-Origin com a origem correta, o navegador bloqueia a resposta — mas a requisição já chegou ao servidor. Esse detalhe é importante: o servidor processou tudo, só a resposta foi bloqueada no cliente.
Requisições com preflight
Qualquer requisição fora do perfil "simples" — PUT, DELETE, PATCH, Content-Type: application/json, headers customizados como Authorization — dispara um preflight automático.
O navegador envia um OPTIONS antes da requisição real:
OPTIONS /api/users HTTP/1.1
Host: api.meusite.com
Origin: https://app.meusite.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Authorization, Content-Type
O servidor precisa responder com os headers corretos. Se falhar aqui, a requisição real nunca é enviada. É por isso que você vê erros de CORS em chamadas DELETE mesmo que o endpoint exista e funcione via Postman.
Os 4 erros mais comuns com CORS
1. Usar * com credentials: 'include'
res.setHeader('Access-Control-Allow-Origin', '*');
Isso funciona para requisições sem credenciais. Mas quando você precisa enviar cookies de sessão ou o header Authorization, o navegador rejeita a combinação * + credentials. A especificação proíbe explicitamente.
Solução: especifique a origem exata e habilite credentials:
res.setHeader('Access-Control-Allow-Origin', 'https://app.meusite.com');
res.setHeader('Access-Control-Allow-Credentials', 'true');
Atenção:
Access-Control-Allow-Origin: *comcredentials: trueno cliente não gera um aviso silencioso — o navegador rejeita ativamente a resposta com erro no console. Se você usa autenticação por cookie ou JWT via header, sempre especifique a origem exata.
2. Ignorar o preflight no servidor
app.use((req, res, next) => {
if (req.method === 'OPTIONS') return next();
next();
});
Esse middleware deixa o OPTIONS passar sem responder corretamente. O navegador fica esperando uma resposta válida e a requisição falha com timeout ou erro genérico de CORS — sem mensagem útil no console.
Solução: intercepte o OPTIONS e retorne 204 imediatamente com os headers corretos.
3. Registrar o middleware CORS depois das rotas
app.get('/api/data', handler);
app.use(cors());
No Express, middlewares são executados na ordem em que são registrados. Se a rota é definida antes do cors(), as requisições para essa rota nunca passam pelo middleware de CORS.
Regra: sempre registre app.use(cors()) antes de qualquer rota.
4. Não cachear o resultado do preflight
Sem Access-Control-Max-Age, o navegador faz um OPTIONS antes de cada requisição não-simples. Em uma aplicação com muitas chamadas de API, isso duplica as requisições de rede desnecessariamente — sem nenhum ganho de segurança.
Solução: adicione Access-Control-Max-Age: 86400 para cachear o resultado do preflight por 24 horas.
Como configurar CORS corretamente
Node.js com Express
A configuração mais robusta usa a biblioteca cors com lista de origens permitidas:
import cors from 'cors';
const allowedOrigins = [
'http://localhost:3000',
'https://app.meusite.com',
];
app.use(cors({
origin: (origin, callback) => {
if (!origin) return callback(null, true);
if (allowedOrigins.includes(origin)) return callback(null, true);
callback(new Error(`Origem não permitida: ${origin}`));
},
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400,
}));
O !origin cobre requisições sem header Origin — como chamadas de apps mobile, curl e Postman — sem abrir brechas para origens não autorizadas.
Configuração manual (sem biblioteca)
Se você prefere controle total ou está em um ambiente sem npm:
const allowedOrigins = ['http://localhost:3000', 'https://app.meusite.com'];
app.use((req, res, next) => {
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Vary', 'Origin');
}
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Max-Age', '86400');
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}
next();
});
O header Vary: Origin é importante aqui: ele instrui caches intermediários (CDN, proxy) a não servirem a mesma resposta para origens diferentes. Sem ele, uma CDN pode entregar a resposta CORS de uma origem para outra, causando erros silenciosos em produção.
Next.js (App Router)
Para rotas individuais:
export async function OPTIONS() {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': 'https://app.meusite.com',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
},
});
}
export async function GET() {
return Response.json(
{ data: 'ok' },
{
headers: {
'Access-Control-Allow-Origin': 'https://app.meusite.com',
},
}
);
}
Para configuração global em todas as rotas de API via next.config.js:
const nextConfig = {
async headers() {
return [
{
source: '/api/:path*',
headers: [
{ key: 'Access-Control-Allow-Origin', value: 'https://app.meusite.com' },
{ key: 'Access-Control-Allow-Methods', value: 'GET, POST, PUT, DELETE, OPTIONS' },
{ key: 'Access-Control-Allow-Headers', value: 'Content-Type, Authorization' },
{ key: 'Access-Control-Max-Age', value: '86400' },
],
},
];
},
};
export default nextConfig;
Proxy no Vite (ambiente de desenvolvimento)
Em desenvolvimento, a solução mais limpa não mexe no servidor: você configura um proxy no Vite que faz as requisições parecerem mesma origem para o navegador.
import { defineConfig } from 'vite';
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
});
Do ponto de vista do navegador, localhost:5173/api/users e localhost:5173/alguma-pagina são mesma origem. O Vite repassa a requisição para localhost:8080/users nos bastidores, sem que o navegador saiba.
Proxy reverso com Nginx (produção)
A solução mais elegante em produção: se o Nginx serve tanto o frontend quanto o backend no mesmo domínio, o navegador nunca precisa fazer requisições cross-origin.
server {
listen 443 ssl;
server_name meusite.com;
location / {
root /var/www/frontend/dist;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://backend:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Para o navegador, tudo está em meusite.com. Não há cross-origin. Não há CORS. É a abordagem mais limpa — e também elimina a necessidade de configurar CORS no backend para uso interno.
Referência rápida de headers CORS
| Header | Direção | Função |
|---|---|---|
Access-Control-Allow-Origin | Resposta | Define qual origem pode acessar o recurso |
Access-Control-Allow-Methods | Resposta | Lista os métodos HTTP permitidos |
Access-Control-Allow-Headers | Resposta | Define quais headers o cliente pode enviar |
Access-Control-Allow-Credentials | Resposta | Autoriza envio de cookies e auth headers |
Access-Control-Max-Age | Resposta | Tempo (em segundos) para cachear o preflight |
Access-Control-Expose-Headers | Resposta | Headers da resposta acessíveis via JavaScript |
Origin | Requisição | Origem da requisição, enviado automaticamente pelo navegador |
Vary | Resposta | Instrui caches a variar a resposta por origem |
Checklist de configuração CORS
Antes de marcar como resolvido, verifique cada item:
| Item | O que verificar |
|---|---|
| ✅ Middleware antes das rotas | app.use(cors()) está registrado antes de qualquer app.get/post/...? |
| ✅ Origens explícitas | A lista de origens permitidas está definida? Sem * em produção? |
| ✅ Preflight tratado | O método OPTIONS responde com 204 e os headers corretos? |
| ✅ Credentials com origem exata | Se credentials: true, a origem é específica — nunca *? |
| ✅ Max-Age configurado | Access-Control-Max-Age está definido para reduzir preflights repetidos? |
| ✅ Vary: Origin presente | Na configuração manual, Vary: Origin está nos headers de resposta? |
| ✅ Proxy avaliado | Em produção, um proxy reverso (Nginx) não eliminaria o CORS completamente? |
FAQ — Perguntas frequentes sobre CORS
Por que o Postman funciona mas o navegador bloqueia?
Porque CORS é uma política do navegador, não do servidor. O Postman e o curl são ferramentas de linha de comando que não implementam a política Same-Origin — eles enviam requisições diretamente sem verificar headers CORS. O navegador, por outro lado, verifica os headers de resposta antes de disponibilizar os dados para o JavaScript da página. Se o servidor não incluir os headers corretos, o navegador bloqueia o acesso — mesmo que a resposta tenha chegado com status 200.
Por que Access-Control-Allow-Origin: * não funciona com cookies?
A especificação CORS proíbe explicitamente a combinação de * com credentials: true. O motivo é segurança: se qualquer origem pudesse enviar requisições autenticadas (com cookies de sessão ou tokens de auth), um site malicioso poderia fazer chamadas autenticadas em nome do usuário — exatamente o ataque que o CORS existe para prevenir. Para usar credenciais, você precisa especificar a origem exata.
O que é preflight e quando ele acontece?
Preflight é uma requisição OPTIONS automática que o navegador envia antes de requisições "não-simples" — qualquer coisa que use PUT, DELETE, PATCH, Content-Type: application/json ou headers customizados como Authorization. O navegador pergunta ao servidor: "você aceita esse tipo de requisição dessa origem?" Se o servidor responder corretamente, a requisição real é enviada. Se não responder ou responder com headers errados, a requisição real nunca acontece.
Qual a diferença entre CORS e CSP (Content Security Policy)?
São mecanismos diferentes com objetivos diferentes. CORS controla quais origens externas podem fazer requisições para o seu servidor — é sobre quem pode chamar sua API. CSP controla quais recursos (scripts, imagens, fontes) uma página pode carregar — é sobre o que a sua página pode executar. Uma API configura CORS para seus consumidores; um site configura CSP para proteger seus próprios usuários de scripts maliciosos.
Como debugar um erro de CORS sem mensagem clara?
Três passos: primeiro, abra a aba Network do DevTools e filtre por OPTIONS — veja se o preflight está sendo enviado e qual a resposta do servidor. Segundo, inspecione os headers da resposta na aba "Headers" da requisição que falhou e compare com o que o navegador espera. Terceiro, se a mensagem de erro for genérica, tente a mesma requisição via curl -v para ver exatamente quais headers o servidor retorna — isso isola se o problema está no servidor ou na configuração CORS.
Conclusão
CORS não é um obstáculo arbitrário — é um mecanismo de proteção que existe para impedir que sites maliciosos façam requisições autenticadas em nome do usuário. Quando você entende isso, a configuração deixa de ser tentativa e erro.
O fluxo de decisão é direto: em desenvolvimento, use proxy no Vite ou webpack e não mexa no servidor. Em produção, prefira um proxy reverso no Nginx — se o contexto permitir — porque isso elimina o problema na raiz. Quando CORS no backend for inevitável, configure com lista de origens explícita, trate o preflight corretamente e adicione Max-Age para não degradar performance.
Com esses fundamentos claros, aquele erro Cross-Origin Request Blocked vai virar uma checklist de 30 segundos — não mais uma hora de tentativa e erro no Stack Overflow.