CORS em JavaScript: Guia Completo para Entender e Resolver Erros Cross-Origin (2026)
← Voltar para Codeshort

CORS em JavaScript: Guia Completo para Entender e Resolver Erros Cross-Origin (2026)

CORS não é bug — é uma política de segurança do navegador. Entenda o mecanismo real por trás dos erros Cross-Origin e veja como configurar corretamente em Express, Next.js, Nginx e Vite.

DC
Dev Code Software
27 de março de 2026·9 min de leitura

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 é)

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:

ComponenteExemplo AExemplo BMesma origem?
Protocolohttphttps
Domínioapi.meusite.commeusite.com
Porta30008080

Qualquer diferença em qualquer um desses três elementos cria origens distintas — e o CORS entra em ação.

Por que curl e 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: * com credentials: true no 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

HeaderDireçãoFunção
Access-Control-Allow-OriginRespostaDefine qual origem pode acessar o recurso
Access-Control-Allow-MethodsRespostaLista os métodos HTTP permitidos
Access-Control-Allow-HeadersRespostaDefine quais headers o cliente pode enviar
Access-Control-Allow-CredentialsRespostaAutoriza envio de cookies e auth headers
Access-Control-Max-AgeRespostaTempo (em segundos) para cachear o preflight
Access-Control-Expose-HeadersRespostaHeaders da resposta acessíveis via JavaScript
OriginRequisiçãoOrigem da requisição, enviado automaticamente pelo navegador
VaryRespostaInstrui caches a variar a resposta por origem

Checklist de configuração CORS

Antes de marcar como resolvido, verifique cada item:

ItemO que verificar
Middleware antes das rotasapp.use(cors()) está registrado antes de qualquer app.get/post/...?
Origens explícitasA lista de origens permitidas está definida? Sem * em produção?
Preflight tratadoO método OPTIONS responde com 204 e os headers corretos?
Credentials com origem exataSe credentials: true, a origem é específica — nunca *?
Max-Age configuradoAccess-Control-Max-Age está definido para reduzir preflights repetidos?
Vary: Origin presenteNa configuração manual, Vary: Origin está nos headers de resposta?
Proxy avaliadoEm 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.