“Access to fetch has been blocked by CORS policy” é provavelmente o erro mais pesquisado por quem começa a consumir APIs pelo navegador, e um dos piores explicados. A correção costuma ser uma linha de configuração, mas no servidor errado, do jeito errado, vira gambiarra que quebra em produção. Este guia explica por que o navegador bloqueia a requisição, como o mecanismo funciona de verdade (incluindo o preflight) e como resolver o erro em Express, PHP, Nginx e Spring Boot.
O que é CORS?
CORS (Cross-Origin Resource Sharing) é o mecanismo que permite a um servidor autorizar requisições vindas de origens diferentes da dele. Por padrão, o navegador aplica a same-origin policy: o JavaScript de um site só pode ler respostas de requisições pro próprio domínio. O CORS é a exceção controlada: o servidor declara, via headers HTTP, quais origens externas podem acessar seus recursos.
Same-origin policy: por que o navegador bloqueia
Imagine que você está logado no internet banking e abre um site malicioso em outra aba. Sem a same-origin policy, o JavaScript desse site poderia fazer requisições pro banco (levando seus cookies de sessão junto) e ler as respostas: saldo, extrato, dados pessoais. O bloqueio existe pra proteger o usuário, não o servidor.
Duas URLs só são da mesma origem quando protocolo, domínio e porta coincidem:
| Origem A | Origem B | Mesma origem? |
|---|---|---|
| https://app.site.com.br | https://app.site.com.br/painel | ✅ (só muda o path) |
| https://app.site.com.br | http://app.site.com.br | ❌ protocolo diferente |
| https://app.site.com.br | https://api.site.com.br | ❌ subdomínio diferente |
| http://localhost:3000 | http://localhost:8000 | ❌ porta diferente |
A última linha explica por que o erro de CORS é praticamente um rito de passagem: front em localhost:3000, API em localhost:8000, origens diferentes.
Anatomia do erro “blocked by CORS policy”
Um detalhe que poupa horas de debug: na maioria dos casos, a requisição chega ao servidor e é processada. O que o navegador bloqueia é a leitura da resposta pelo JavaScript, porque ela veio sem o header de autorização. Por isso o erro não aparece no log do servidor: pra ele, foi uma requisição normal com status 200.
No DevTools, abra a aba Network: se a requisição aparece com status 200 mas o console grita CORS, falta header na resposta. Se aparece uma requisição OPTIONS falhando antes da sua, o problema é o preflight. Pra reproduzir fora do navegador: curl -H "Origin: http://localhost:3000" -i https://sua-api.com/dados e confira se a resposta traz Access-Control-Allow-Origin.
Como o CORS funciona na prática
Requisições simples vs preflight
Requisições “simples” (GET, HEAD, POST com Content-Type de formulário, sem headers customizados) vão direto: o navegador envia, recebe e só então decide se o JavaScript pode ler a resposta, conferindo o Access-Control-Allow-Origin.
Todo o resto exige preflight: antes da requisição real, o navegador envia um OPTIONS perguntando se a operação é permitida. E aqui mora a pegadinha clássica: Content-Type: application/json não é “simples”, então praticamente toda chamada de API REST moderna dispara preflight.
// 1. Preflight do navegador (automático)
OPTIONS /clientes HTTP/1.1
Origin: http://localhost:3000
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, authorization
// 2. Resposta do servidor autorizando
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
// 3. Só agora o navegador envia o POST real
O Max-Age diz por quanto tempo o navegador pode guardar essa permissão sem repetir o preflight: configurá-lo corta metade das requisições OPTIONS.

Os headers Access-Control-*
| Header | Função |
|---|---|
Access-Control-Allow-Origin |
origem autorizada (ou *) |
Access-Control-Allow-Methods |
métodos permitidos no preflight |
Access-Control-Allow-Headers |
headers que a requisição real pode enviar |
Access-Control-Allow-Credentials |
permite cookies/autenticação (incompatível com *) |
Access-Control-Expose-Headers |
headers da resposta visíveis ao JavaScript |
Access-Control-Max-Age |
cache do preflight, em segundos |
Como resolver o erro de CORS (por stack)
A regra de ouro antes do código: o CORS se resolve no servidor que recebe a requisição, nunca no front-end. Se o erro aparece chamando a SUA API, configure-a como abaixo. Se a API é de terceiros, pule pra seção do proxy.
Node.js / Express
const cors = require('cors');
app.use(cors({
origin: ['https://app.seusite.com.br', 'http://localhost:3000'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400
}));
PHP (puro)
$permitidas = ['https://app.seusite.com.br', 'http://localhost:3000'];
$origem = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origem, $permitidas)) {
header("Access-Control-Allow-Origin: $origem");
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Access-Control-Max-Age: 86400');
}
// preflight termina aqui, sem corpo
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
No Laravel, o middleware HandleCors já vem instalado: configure as origens em config/cors.php.
Nginx
location /api/ {
add_header Access-Control-Allow-Origin "https://app.seusite.com.br" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
if ($request_method = OPTIONS) {
add_header Access-Control-Max-Age 86400;
return 204;
}
proxy_pass http://backend;
}
Spring Boot
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://app.seusite.com.br")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("Content-Type", "Authorization")
.maxAge(86400);
}
}
Quando você não controla o servidor: proxy
Se a API é de terceiros e não envia os headers, nenhuma configuração no seu front resolve, porque a decisão é do servidor deles. A saída é o proxy: o CORS é imposto pelo navegador, então basta a requisição partir de um servidor seu. O fluxo vira navegador → seu backend (mesma origem) → API externa, e o navegador nunca vê a origem cruzada.
Em desenvolvimento, o Vite resolve com três linhas (server.proxy no vite.config.js); em produção, um proxy_pass no Nginx ou uma rota no seu backend cumprem o papel. De quebra, o proxy esconde sua API key do navegador, o que já deveria acontecer de qualquer forma: chamar API de terceiros com token direto do front expõe a credencial a qualquer um que abrir o DevTools.

5 erros clássicos de configuração CORS
credentials: truecomAllow-Origin: *: combinação proibida pela especificação. Com cookies, a origem precisa ser explícita.- Não responder ao OPTIONS: a rota existe pro POST, mas o framework devolve 404/405 pro preflight, e a requisição real nunca acontece.
- Header duplicado: Nginx adiciona, aplicação adiciona de novo, e o navegador rejeita
Allow-Origincom dois valores. Configure num lugar só. - Origem
null: páginas abertas viafile://enviamOrigin: null. Teste com um servidor local (npx serve), não com arquivo solto. - “Resolver” com extensão do navegador: extensões que desabilitam CORS só mascaram o problema na sua máquina. Pros seus usuários, o erro continua lá.
CORS não é segurança do servidor
Vale repetir, porque a confusão é comum: CORS protege o usuário do navegador, não a sua API. Requisições de curl, Postman, scripts e outros servidores ignoram CORS completamente, porque não há navegador impondo a política. Liberar Allow-Origin: * numa API pública de leitura é perfeitamente razoável; o que protege seus dados é autenticação e autorização, não a lista de origens.
Perguntas frequentes
O que significa o bloqueio de CORS?
Significa que o JavaScript da sua página tentou ler a resposta de uma requisição pra outra origem (domínio, porta ou protocolo diferentes) e o servidor não enviou o header Access-Control-Allow-Origin autorizando a sua origem. O navegador então impede a leitura da resposta.
Como liberar o CORS no servidor?
Configure o servidor pra responder com Access-Control-Allow-Origin contendo a origem do seu front-end, e responda às requisições OPTIONS (preflight) com os headers Allow-Methods e Allow-Headers. Em Express, o middleware cors faz isso; em PHP, a função header(); no Nginx, a diretiva add_header.
O que é Access-Control-Allow-Origin?
É o header de resposta que informa ao navegador qual origem está autorizada a ler aquela resposta. Pode conter uma origem específica (https://app.site.com.br) ou o curinga *, que libera qualquer origem mas é incompatível com requisições autenticadas por cookie.
Por que o erro de CORS só acontece no navegador?
Porque a same-origin policy é um mecanismo do navegador pra proteger o usuário. Ferramentas como curl e Postman, e chamadas servidor-a-servidor, não aplicam essa política: a mesma requisição que falha no fetch do navegador funciona nelas normalmente.
Usar Access-Control-Allow-Origin: * é seguro?
Pra APIs públicas de leitura, sim: o curinga só permite que páginas leiam respostas que já seriam acessíveis por qualquer outro meio. É inseguro quando a API usa cookies de sessão ou retorna dados privados do usuário; nesses casos, liste as origens explicitamente e proteja os dados com autenticação.
Conclusão
O erro de CORS deixa de assustar quando você entende os três fatos por trás dele: o bloqueio é do navegador (não da API), a correção é no servidor que recebe a requisição (nunca no front) e a requisição em geral chegou ao destino: só a leitura da resposta foi vetada. Com as origens listadas explicitamente, o OPTIONS respondido com 204 e o Max-Age configurado, o CORS vira o que sempre deveria ter sido: uma linha de configuração, não uma tarde perdida.

