Você lança um novo frontend, abre o console, e lá está: um erro CORS vermelho dizendo que a requisição foi “bloqueada pela política CORS”. Sua API funciona bem no Apidog ou no curl, mas o navegador se recusa a entregar a resposta ao seu JavaScript. Frustrante? Sim. Misterioso? Não, uma vez que você sabe onde o erro se encontra.
Aqui está o fato central que a maioria dos tutoriais esconde: um erro CORS é imposto pelo navegador, mas causado pelo servidor. O navegador bloqueia a resposta porque seu servidor não enviou os cabeçalhos Access-Control-Allow-Origin corretos. Portanto, a correção quase sempre acontece na configuração do servidor, não no seu código frontend.
Este guia aborda o que o CORS faz, como funciona a requisição de pré-checagem (preflight), as seis mensagens de erro CORS mais comuns com a correção exata para cada uma, e configurações funcionais para Express, Spring Boot e Nginx. Você também verá como depurar fora do navegador, que é a maneira mais rápida de diferenciar “servidor mal configurado” de “navegador bloqueado”.
O que é um erro CORS (e o que não é)
CORS significa Cross-Origin Resource Sharing (Compartilhamento de Recursos de Origem Cruzada). Por padrão, os navegadores impõem a política de mesma origem: JavaScript em execução em https://app.example.com não pode ler respostas de https://api.example.com, porque o esquema, host ou porta são diferentes. CORS é o mecanismo que os servidores usam para relaxar essa regra intencionalmente. Os detalhes completos estão na documentação CORS do MDN, e o algoritmo subjacente é definido na especificação Fetch.
Três pontos esclarecem a maioria das confusões:
- O navegador impõe. Apenas os navegadores aplicam as verificações CORS. Chamadas de servidor para servidor, curl e clientes de API de desktop o ignoram completamente.
- O servidor configura. O navegador decide com base nos cabeçalhos de resposta que seu servidor envia. Sem cabeçalhos, sem acesso.
- A requisição geralmente ainda chega ao servidor. Para requisições simples, o servidor processa tudo e responde. O navegador então retém a resposta do seu JavaScript. CORS não é uma barreira de segurança em torno da sua API; ele protege os usuários de páginas maliciosas que leem dados de origem cruzada com seus cookies.
Então, quando você vir um erro CORS, não procure por uma solução alternativa no frontend. Leia a mensagem de erro e, em seguida, corrija o cabeçalho ausente ou incorreto no servidor.
Anatomia da requisição de pré-checagem (preflight)
Antes de certas requisições de origem cruzada, o navegador envia um 'batedor': uma requisição OPTIONS chamada preflight (pré-checagem). Ela é disparada quando sua requisição usa métodos além de GET, HEAD ou POST, envia cabeçalhos personalizados como Authorization, ou usa um Content-Type como application/json.
O preflight se parece com isto:
OPTIONS /v1/orders HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization, content-type
O navegador está perguntando: “Uma página em app.example.com quer fazer um POST aqui com estes cabeçalhos. Permitido?” Uma resposta correta do servidor:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400
Vary: Origin
Se alguma parte estiver faltando, o navegador cancela a requisição real antes mesmo de ela ser disparada. Seu endpoint da API nunca é executado, seus logs não mostram nada além de um hit de OPTIONS, e o console exibe um erro CORS. Access-Control-Max-Age informa ao navegador para armazenar em cache este veredito (86400 segundos aqui), para que requisições repetidas pulem o preflight.
Mantenha esta dança de dois passos em mente. Metade de toda a depuração CORS se resume a uma pergunta: o preflight falhou ou a requisição real falhou?
Os 6 erros CORS mais comuns e como corrigir cada um
Os navegadores escrevem mensagens de erro CORS surpreendentemente precisas. Compare a sua com a lista abaixo.
1. Nenhum cabeçalho ‘Access-Control-Allow-Origin’ está presente
O clássico. Seu servidor enviou uma resposta sem nenhum cabeçalho CORS. O navegador não tinha nada para avaliar, então bloqueou o acesso.
Correção: Configure o servidor para enviar Access-Control-Allow-Origin com a origem solicitante específica ou * para APIs públicas e sem credenciais:
Access-Control-Allow-Origin: https://app.example.com
Uma armadilha: respostas de erro frequentemente ignoram os cabeçalhos CORS, mesmo quando as respostas de sucesso os incluem. Se sua API retornar um 500 e o middleware apenas decorar 200s, o console exibirá um erro CORS em vez do erro real do servidor. Certifique-se de que os cabeçalhos CORS estejam anexados a cada resposta, incluindo páginas 403 Forbidden e 500.
2. Wildcard ‘*’ não pode ser usado com credenciais
A mensagem diz: “O valor do cabeçalho ‘Access-Control-Allow-Origin’ não deve ser o wildcard ‘*’ quando o modo de credenciais da requisição é ‘include’.”
Seu frontend envia cookies ou cabeçalhos de autenticação com credentials: 'include', mas o servidor responde com Access-Control-Allow-Origin: *. A especificação Fetch proíbe essa combinação; um wildcard mais credenciais permitiria que qualquer site na internet lesse respostas autenticadas.
Correção: Repita a origem exata em vez do wildcard, e adicione o cabeçalho de credenciais:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Valide a Origin de entrada contra uma lista de permissões antes de repeti-la. Refletir origens arbitrárias com credenciais habilitadas anula toda a proteção.
3. Resposta à requisição de pré-checagem (preflight) não passa na verificação de controle de acesso
Seu servidor nunca tratou a requisição OPTIONS. Talvez a rota apenas defina POST, então OPTIONS retorna um 404 ou 405. Talvez um middleware de autenticação a rejeitou com um 401 porque o preflight não carrega nenhum token (navegadores nunca anexam credenciais a preflights).
Correção: Trate OPTIONS explicitamente e retorne um 2xx com o conjunto completo de cabeçalhos CORS antes que a autenticação seja executada. Na maioria dos frameworks, montar o middleware CORS primeiro resolve. Se você estiver escrevendo manualmente:
app.options('/v1/orders', (req, res) => {
res.set({
'Access-Control-Allow-Origin': 'https://app.example.com',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Authorization, Content-Type'
});
res.sendStatus(204);
});
4. O valor do cabeçalho não é igual à origem fornecida
O servidor envia um cabeçalho Access-Control-Allow-Origin, mas ele nomeia a origem errada. Causas comuns: uma origem de produção hardcoded enquanto você testa de http://localhost:5173, uma comparação de lista de permissões falhando em http vs https, ou uma barra final errante (https://app.example.com/ não é um valor de origem válido).
Correção: Compare o cabeçalho Origin da requisição com sua lista de permissões exatamente, repita a correspondência e envie Vary: Origin para que caches e CDNs não sirvam o cabeçalho de uma origem para outra:
const allowed = ['https://app.example.com', 'http://localhost:5173'];
if (allowed.includes(req.headers.origin)) {
res.set('Access-Control-Allow-Origin', req.headers.origin);
res.set('Vary', 'Origin');
}
5. Campo de cabeçalho da requisição ou método não permitido
Duas mensagens irmãs: “Campo de cabeçalho da requisição authorization não é permitido por Access-Control-Allow-Headers na resposta do preflight” e “Método PUT não é permitido por Access-Control-Allow-Methods.”
O preflight foi bem-sucedido, mas sua resposta não cobriu o que sua requisição precisa. Você adicionou um cabeçalho Authorization ou um X-Request-Id, e a lista de permissões do servidor nunca o mencionou.
Correção: Estenda a resposta do preflight para incluir cada cabeçalho e método que seu frontend envia:
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type, X-Request-Id
Os nomes dos cabeçalhos aqui não diferenciam maiúsculas de minúsculas. Os métodos diferenciam maiúsculas de minúsculas e são em maiúsculas.
6. Redirecionamento não é permitido para uma requisição de pré-checagem (preflight)
O preflight atingiu uma URL retornando 301 ou 302, e os navegadores se recusam a seguir redirecionamentos durante o preflight. Culpados típicos: uma URL http redirecionando para https, uma barra final ausente que seu framework “utilmente” redireciona, ou um gateway redirecionando /v1/orders para /v1/orders/.
Correção: Aponte seu frontend diretamente para a URL final. Use https desde o início, combine a convenção de barra final do seu roteador e confirme com uma chamada OPTIONS manual para verificar se o endpoint responde com um 2xx em vez de um 3xx.
Exemplos de configuração do servidor
Aqui está a configuração CORS correta em três stacks comuns.
Express
Use o middleware cors oficial em vez de criar os cabeçalhos manualmente:
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors({
origin: ['https://app.example.com', 'http://localhost:5173'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Authorization', 'Content-Type'],
credentials: true,
maxAge: 86400
}));
Monte-o antes do seu middleware de autenticação para que os preflights nunca sejam rejeitados por falta de tokens. Desenvolvedores Python obtêm o mesmo padrão da extensão Flask-CORS, que encapsula a lógica de cabeçalho idêntica para aplicativos Flask.
Spring Boot
Configuração global via WebMvcConfigurer:
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/v1/**")
.allowedOrigins("https://app.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("Authorization", "Content-Type")
.allowCredentials(true)
.maxAge(86400);
}
}
Usando Spring Security? Chame .cors(Customizer.withDefaults()) na sua cadeia de filtros de segurança também, ou a camada de segurança bloqueará os preflights antes mesmo que a configuração MVC os veja. Consulte a documentação CORS do Spring para o conjunto completo de opções.
Nginx
Quando o Nginx termina as requisições na frente do seu aplicativo, responda aos preflights na borda:
location /v1/ {
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin "https://app.example.com" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
add_header Access-Control-Max-Age 86400 always;
return 204;
}
add_header Access-Control-Allow-Origin "https://app.example.com" always;
add_header Vary "Origin" always;
proxy_pass http://backend;
}
A flag always é importante. Sem ela, o Nginx ignora as diretivas add_header em respostas 4xx e 5xx, o que recria o erro número um em cada requisição falha. E escolha uma camada para gerenciar o CORS: se tanto o Nginx quanto seu aplicativo adicionarem cabeçalhos, os navegadores verão duplicatas como Access-Control-Allow-Origin: *, * e rejeitarão a resposta.
Depure CORS fora do navegador com Apidog
O erro do console informa que o navegador bloqueou algo. Ele não diz o que o servidor enviou. A maneira mais rápida de ver a verdade é tirar o navegador do circuito.
Apidog é um cliente de API de desktop, então suas requisições não estão sujeitas a verificações CORS do navegador. Isso lhe dá um experimento limpo: envie a mesma requisição do Apidog que seu frontend estava fazendo. Se ela for bem-sucedida lá, sua lógica de API está boa e o problema é puramente a falta de cabeçalhos CORS. Se falhar lá também, você tem um bug comum na API disfarçado de CORS, e técnicas gerais de teste de API se aplicam.
Uma sessão de depuração CORS no Apidog se parece com isto:
- Repita a requisição real. Copie a requisição falha da aba Rede do seu navegador e recrie-a no Apidog com o mesmo método, cabeçalhos e corpo. Verifique o status e o corpo. Um 500 aqui significa que CORS nunca foi seu problema.
- Teste o preflight manualmente. Crie uma nova requisição, defina o método como
OPTIONSe adicione os cabeçalhos que um navegador enviaria:Origin: https://app.example.com,Access-Control-Request-Method: POSTeAccess-Control-Request-Headers: authorization, content-type. Envie-a. - Inspecione os cabeçalhos da resposta. No painel de resposta, procure por
Access-Control-Allow-Origin,Access-Control-Allow-MethodseAccess-Control-Allow-Headers. Compare cada valor com o que seu frontend precisa. Um cabeçalho ausente, uma origem errada ou um status 3xx salta à vista imediatamente, sem a necessidade de adivinhação no console. - Verifique a correção. Após alterar a configuração do servidor, reenvie a mesma requisição
OPTIONSsalva e observe os cabeçalhos serem atualizados. Sem necessidade de reimplantar frontends, sem rituais de limpeza de cache.
Este fluxo de trabalho também resolve o eterno argumento “funciona no meu cliente de API, falha no navegador” em segundos, o mesmo enigma por trás da pergunta de teste CORS do Postman. O cliente funciona porque ignora o CORS. O navegador falha porque seu servidor não pronunciou as palavras mágicas. Baixe o Apidog gratuitamente e mantenha a requisição OPTIONS salva ao lado de seus testes de endpoint regulares; futuros incêndios CORS serão apagados em um clique.
Uma checklist CORS de 30 segundos
Antes de registrar o bug, revise esta lista:
- A resposta com falha inclui
Access-Control-Allow-Origin? - Seu valor corresponde exatamente à origem da sua página (esquema, host, porta, sem barra final)?
- Usando cookies ou autenticação? Confirme uma origem específica mais
Access-Control-Allow-Credentials: true, nunca*. OPTIONSretorna um 2xx com métodos e cabeçalhos que cobrem sua requisição?- Algum redirecionamento na URL do preflight?
- As respostas de erro (401, 403, 500) carregam os mesmos cabeçalhos CORS que as respostas de sucesso?
Nove em cada dez vezes, uma dessas seis linhas é sua resposta. Verifique com uma requisição OPTIONS manual no Apidog, corrija a configuração do servidor e volte a desenvolver.
FAQ
Por que recebo um erro CORS apenas no navegador?
Porque apenas os navegadores impõem o CORS. A política de mesma origem protege os usuários de páginas maliciosas que leem seus dados autenticados, então os navegadores verificam Access-Control-Allow-Origin em cada resposta de origem cruzada. curl, serviços de backend e clientes de desktop não possuem tal regra. Se uma requisição é bem-sucedida em todos os lugares, exceto no navegador, seu servidor está faltando ou configurando incorretamente os cabeçalhos CORS; a própria API está saudável.
O CORS se aplica ao Postman ou Apidog?
Não. Postman e Apidog são aplicativos de desktop, não páginas web executadas dentro de uma sandbox de navegador, então suas requisições ignoram completamente o CORS. Isso é precisamente o que os torna úteis para depuração CORS: eles mostram os cabeçalhos de resposta brutos do servidor sem a filtragem do navegador. A confusão do teste CORS do Postman geralmente começa aqui; uma requisição bem-sucedida em um cliente de desktop não prova nada sobre o comportamento do navegador, mas isola a camada que está falhando.
Um erro CORS é um recurso de segurança ou um bug?
Um recurso. Erros CORS significam que o navegador está fazendo seu trabalho: recusando-se a expor dados de resposta de origem cruzada a scripts, a menos que o servidor consinta. Desativar o CORS no navegador com flags ou extensões esconde o sintoma na sua máquina enquanto cada usuário ainda encontra o problema. Corrija os cabeçalhos do servidor em vez disso.
Posso usar Access-Control-Allow-Origin: * em todos os lugares?
Apenas para APIs públicas e somente leitura, sem cookies ou autenticação. O wildcard é rejeitado sempre que credenciais são incluídas, e ele anuncia que seus dados estão abertos para qualquer origem na web. Para qualquer coisa autenticada, mantenha uma lista de permissões de origem, repita a origem correspondente e envie Vary: Origin para que caches compartilhados mantenham as respostas separadas.
