Erro de CORS: o que é e como resolver (guia com exemplos)

“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.

Diagrama de sequência do CORS: preflight OPTIONS e requisição real
O preflight pede permissão; sem os headers certos, o navegador bloqueia a leitura

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')&#59;

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
}))&#59;

PHP (puro)

$permitidas = ['https://app.seusite.com.br', 'http://localhost:3000']&#59;
$origem = $_SERVER['HTTP_ORIGIN'] ?? ''&#59;

if (in_array($origem, $permitidas)) {
    header("Access-Control-Allow-Origin: $origem")&#59;
    header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE')&#59;
    header('Access-Control-Allow-Headers: Content-Type, Authorization')&#59;
    header('Access-Control-Max-Age: 86400')&#59;
}
// preflight termina aqui, sem corpo
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(204)&#59;
    exit&#59;
}

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&#59;
    add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE" always&#59;
    add_header Access-Control-Allow-Headers "Content-Type, Authorization" always&#59;

    if ($request_method = OPTIONS) {
        add_header Access-Control-Max-Age 86400&#59;
        return 204&#59;
    }
    proxy_pass http://backend&#59;
}

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)&#59;
    }
}

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.

Escudo do navegador controlando a passagem de requisições
CORS protege o usuário no navegador: requisições de curl e servidores passam direto

5 erros clássicos de configuração CORS

  • credentials: true com Allow-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-Origin com dois valores. Configure num lugar só.
  • Origem null: páginas abertas via file:// enviam Origin: 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.

Leituras relacionadas

Compartilhe nas mídias: