Erro de CORS (blocked by CORS policy): como resolver

Publicado:

17 min de leitura

Acar Diveroli
Autor: Acar Diveroli
Bloco ORIGIN ligado por cabos a fetch(), PREFLIGHT, CDN e API; só o cabo da API é azul, os outros três estão cortados

O seu front-end (um app React com Vite, por exemplo) roda em http://localhost:5173, e a sua API em http://localhost:3001. A API devolve JSON no curl e no Postman, mas no navegador o fetch() lança TypeError: Failed to fetch e o console mostra: Access to fetch at 'http://localhost:3001/api/products' from origin 'http://localhost:5173' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. Mesmo assim, o log da API mostra que a requisição chegou e foi respondida com 200.

A seguir: o que é uma origem, por que o navegador bloqueia a resposta, requisições simples e requisições preflight, cada mensagem do Chrome, correções testadas para Express, Flask e Vite, uma rota no back-end para APIs que você não controla e por que código de scraping nunca esbarra no CORS.

O que é um erro de CORS?

É o navegador se recusando a entregar uma resposta ao seu script. CORS, Cross-Origin Resource Sharing (compartilhamento de recursos entre origens), é um conjunto de cabeçalhos de resposta HTTP com os quais um servidor diz ao navegador quais outras origens podem ler as respostas dele. O Fetch Standard define esse mecanismo como uma permissão que o servidor precisa liberar de forma explícita, para que dados atrás de um firewall ou de um login não vazem para outros sites por padrão. Sem cabeçalhos que batam, o navegador aplica a regra padrão, a política de mesma origem (same-origin policy), e bloqueia a leitura.

Daí saem três consequências. Os logs do servidor parecem normais, porque quem gerou o erro foi o navegador. O seu código recebe só uma falha genérica, TypeError: Failed to fetch do fetch() ou AxiosError: Network Error (código ERR_NETWORK) do Axios, e o motivo aparece apenas no console. E a correção fica no servidor que responde, não no código que pergunta.

O que é uma origem e o que a política de mesma origem bloqueia?

Uma origem (origin) é o esquema, o host e a porta de uma URL. Duas URLs têm a mesma origem só quando os três coincidem:

  • http://localhost:5173 e http://localhost:3001: portas diferentes, origens diferentes.
  • http://example.com e https://example.com: esquemas diferentes.
  • https://example.com e https://api.example.com: hosts diferentes; um subdomínio é uma origem própria.
  • https://example.com/shop e https://example.com/api: mesma origem; o caminho não conta.

A política de mesma origem continua deixando uma página incorporar imagens, scripts e folhas de estilo de outros sites, criar links para eles e enviar formulários a eles. O que ela impede é a leitura: um script não pode ler uma resposta de outra origem, a menos que essa origem permita (MDN: Same-origin policy). O CORS é a forma de o servidor dar essa permissão.

Como funciona uma requisição entre origens, passo a passo?

  1. A sua página na origem A chama fetch() para uma URL na origem B.
  2. O navegador verifica se a requisição é "simples". Se não for, ele envia antes um preflight (próxima seção).
  3. Ele adiciona um cabeçalho Origin, como Origin: http://localhost:5173. Scripts não podem definir nem remover esse cabeçalho.
  4. O servidor executa o código dele e responde. O que o código fez já aconteceu.
  5. O navegador compara o Access-Control-Allow-Origin da resposta com a origem da página. Ele aceita uma correspondência exata, ou * quando nenhuma credencial, como cookies, foi enviada.
  6. Se bater, o seu código recebe a resposta; se não, o navegador descarta a resposta, a promise é rejeitada e o console informa o motivo.

O passo 4 é o que as pessoas esquecem: o CORS não impede que as requisições cheguem ao servidor, e curl, scripts e outros servidores pulam o passo 5 por completo. Ele protege os visitantes, para que uma página que eles abrem não consiga ler os dados deles em outros sites usando os cookies deles; ele não protege a sua API.

Requisições simples e preflight: por que o GET funciona e o POST falha

Uma requisição é "simples" quando usa GET, HEAD ou POST e só cabeçalhos da lista segura: Accept, Accept-Language, Content-Language, Range e Content-Type com application/x-www-form-urlencoded, multipart/form-data ou text/plain. Todo o resto passa antes por um preflight (verificação prévia): uma requisição OPTIONS com Access-Control-Request-Method e Access-Control-Request-Headers, depois da qual o navegador só envia a requisição real se a resposta permitir (MDN: CORS). Um corpo JSON, um cabeçalho Authorization, PUT, PATCH, DELETE ou um cabeçalho personalizado como X-Request-ID disparam um preflight.

Enviamos um GET simples e um POST com corpo JSON de uma página na porta 5173 para uma API na porta 3001 que não envia cabeçalhos CORS. A API registrou:

text
[no-cors-api] GET /api/products origin=http://localhost:5173
[no-cors-api] OPTIONS /api/products origin=http://localhost:5173

O GET foi respondido com 200 e os dados, e mesmo assim o navegador o bloqueou. Do POST, só o preflight chegou; ele falhou, então o POST nunca foi enviado. Já um POST simples entre origens, como um enviado em formato de formulário, chega ao servidor e é executado mesmo que a página veja um erro. Por isso, proteja essas ações com autenticação e tokens CSRF, não com CORS.

O Access-Control-Max-Age permite que o navegador guarde em cache a resposta de um preflight. O padrão do Fetch Standard é de 5 segundos; o Chromium limita o valor a 2 horas, e o Firefox a 24 horas.

O que significa cada mensagem "blocked by CORS policy"?

Toda mensagem do Chrome começa com Access to fetch at '<URL>' from origin '<origin>' has been blocked by CORS policy:, ou com Access to XMLHttpRequest at no Axios e no XMLHttpRequest. O Chromium mantém esses textos fixos em inglês no código-fonte, por isso eles aparecem em inglês também em um navegador em português. O texto depois dos dois-pontos é a causa. As cinco primeiras linhas da tabela apareceram palavra por palavra no nosso teste com um navegador baseado no Chromium 152; as demais vêm do código-fonte do Chromium.

Mensagem depois de "blocked by CORS policy:"SignificadoCorreção
No 'Access-Control-Allow-Origin' header is present on the requested resource.Falta o cabeçalho CORS: não foi configurado, ou uma página de erro respondeuPermita a sua origem; confira o status real
Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present…A resposta ao OPTIONS não tinha cabeçalho CORS; a requisição real não foi enviadaResponda ao OPTIONS com cabeçalhos CORS
The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.Cookies enviados, o servidor respondeu *Origem exata mais Access-Control-Allow-Credentials: true
Request header field x-debug is not allowed by Access-Control-Allow-Headers in preflight response.Um cabeçalho que você envia não é permitidoPermita o cabeçalho ou pare de enviá-lo
Method PATCH is not allowed by Access-Control-Allow-Methods in preflight response.O método não é permitidoAdicione o método
The 'Access-Control-Allow-Origin' header has a value '…' that is not equal to the supplied origin.Outra origem está liberada: porta errada, http, barra no finalCorrija a lista de permissões
The 'Access-Control-Allow-Origin' header contains multiple values '…', but only one is allowed.Duas camadas adicionam o cabeçalho, como o app e o nginxDefina o cabeçalho em um só lugar
Response to preflight request doesn't pass access control check: It does not have HTTP ok status.O OPTIONS recebeu 401, 404, 405 ou 500Deixe o OPTIONS passar antes da autenticação
Response to preflight request doesn't pass access control check: Redirect is not allowed for a preflight request.O OPTIONS foi redirecionado, por exemplo para https ou para uma página de loginChame a URL final

Versões antigas do Chrome adicionavam uma frase sugerindo mode: 'no-cors'; o Chromium a removeu em março de 2025 porque ela confundia as pessoas (as perguntas frequentes explicam por quê). O Firefox escreve Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at … (Reason: CORS header 'Access-Control-Allow-Origin' missing).

Como encontrar a causa real?

Abra as ferramentas de desenvolvedor (F12) e leia a linha na aba Console (o nome é o mesmo na interface em português): URL, origem, motivo. Na aba Network (Rede em português), a requisição bloqueada mostra CORS error (Erro de CORS) na coluna Status, e uma chamada com preflight tem uma entrada OPTIONS separada cuja coluna Initiator (Iniciador) diz Preflight (Simulação na interface em português). Clique nela para ver os cabeçalhos.

O código de status real passa despercebido com facilidade. No nosso teste, um gateway respondeu 502 sem cabeçalhos CORS: o console mostrou só a linha "No 'Access-Control-Allow-Origin' header", enquanto curl -i na mesma URL imprimiu HTTP/1.1 502 Bad Gateway. Páginas de erro do nginx, de balanceadores de carga ou de um app que caiu raramente trazem cabeçalhos CORS, então quedas parecem problemas de CORS. A diretiva add_header do nginx só se aplica a respostas 200, 201, 204, 206, 301, 302, 303, 304, 307 e 308, a menos que você adicione o parâmetro always (documentação do nginx).

Como resolver um erro de CORS quando a API é sua?

Envie os cabeçalhos pela própria API, para as origens exatas do seu front-end. Testamos cada trecho em 6 de outubro de 2026 com Node.js 24.11, Express 5.2.1, cors 2.8.6, Vite 8.3.3, Python 3.13, Flask 3.1.3 e Flask-CORS 6.0.5.

Express

Instale com npm install express cors e defina "type": "module" no package.json.

js
// server.js: uma API que permite um único front-end (Express 5, cors 2.8)
import express from "express";
import cors from "cors";

const app = express();

app.use(cors({
  origin: ["https://app.example.com", "http://localhost:5173"],
  methods: ["GET", "POST", "PUT", "DELETE"],
  allowedHeaders: ["Content-Type", "Authorization"],
  credentials: true,
  maxAge: 600,
}));
app.use(express.json());

app.get("/api/products", (req, res) => {
  res.json([{ id: 1, name: "Desk lamp" }]);
});

app.post("/api/products", (req, res) => {
  res.status(201).json({ created: req.body });
});

app.listen(3000, () => console.log("API on http://localhost:3000"));

Registrado com app.use() antes das rotas, o middleware também responde a todo preflight OPTIONS (middleware cors do Express). Escreva as origens exatamente como o navegador as envia, sem barra no final. Outras origens não recebem cabeçalho Access-Control-Allow-Origin, e a ideia é justamente essa. Mantenha credentials: true só se o front-end enviar cookies com credentials: "include".

Confira o preflight em um terminal:

bash
curl -i -X OPTIONS http://localhost:3000/api/products \
  -H "Origin: http://localhost:5173" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: content-type"

As linhas relevantes da nossa execução:

text
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:5173
Vary: Origin
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET,POST,PUT,DELETE
Access-Control-Allow-Headers: Content-Type,Authorization
Access-Control-Max-Age: 600

O Vary: Origin avisa caches e CDNs de que a resposta depende do cabeçalho Origin. Com Origin: https://evil.example, o mesmo comando retorna 204, mas sem a linha Access-Control-Allow-Origin.

Flask

Instale com pip install flask flask-cors.

python
# app.py: a mesma API em Flask 3 com Flask-CORS 6
from flask import Flask, jsonify, request
from flask_cors import CORS

app = Flask(__name__)
CORS(
    app,
    resources={r"/api/*": {"origins": ["https://app.example.com", "http://localhost:5173"]}},
    allow_headers=["Content-Type", "Authorization"],
    supports_credentials=True,
    max_age=600,
)


@app.get("/api/products")
def list_products():
    return jsonify([{"id": 1, "name": "Desk lamp"}])


@app.post("/api/products")
def create_product():
    return jsonify({"created": request.get_json()}), 201


if __name__ == "__main__":
    app.run(port=5000)

Na nossa execução, o preflight retornou 200 com a origem, os métodos e Vary: Origin, e o POST da página retornou 201. Sempre passe uma lista de origens junto com supports_credentials=True: sem ela, o Flask-CORS 6.0.5 devolveu para nós uma origem qualquer, https://evil.example, com Access-Control-Allow-Credentials: true.

nginx, gateways e CDNs

Se um proxy reverso define os cabeçalhos no lugar do app, dê a cada add_header o parâmetro always, para que as respostas de erro também os tragam, e deixe só uma camada defini-los. Nunca copie qualquer Origin recebido para a resposta enquanto permite credenciais: qualquer site poderia então ler os dados dos seus usuários com os cookies deles. Compare as origens com uma lista fixa.

Desenvolvimento local: deixe o servidor de desenvolvimento encaminhar a API

Em desenvolvimento, a correção mais simples é colocar a API na mesma origem. O servidor de desenvolvimento do Vite pode encaminhar todo caminho que começa com /api:

js
// vite.config.js: em desenvolvimento, /api passa pelo Vite até a API
import { defineConfig } from "vite";

export default defineConfig({
  server: {
    port: 5173,
    proxy: {
      "/api": {
        target: "http://localhost:3000",
        changeOrigin: true,
      },
    },
  },
});

O front-end chama fetch("/api/products") sem host. O navegador vê a própria origem, então nenhuma verificação de CORS acontece, e o Vite repassa a requisição para a porta 3000; no nosso teste, o GET retornou 200 e o POST 201. O changeOrigin coloca o destino no cabeçalho Host. O webpack-dev-server oferece o mesmo com devServer.proxy. Em produção, sirva o front-end e a API sob uma mesma origem pelo seu servidor web, ou mantenha a configuração de CORS acima.

Quando a API não é sua: chame-a pelo seu back-end

Uma API de terceiros sem cabeçalhos CORS normalmente foi feita para servidores, muitas vezes porque a chave secreta dela nunca pode chegar a um navegador. Adicione uma rota ao seu back-end: o navegador chama a sua origem, e o seu servidor chama a API.

js
// relay.js: o seu back-end chama a API de terceiros; o navegador só fala com você
import express from "express";

const app = express();
const PARTNER_URL = "https://api.partner.example/v1/products";

app.get("/api/partner-products", async (req, res) => {
  try {
    const r = await fetch(PARTNER_URL, {
      headers: { Authorization: `Bearer ${process.env.PARTNER_API_KEY}` },
      signal: AbortSignal.timeout(10_000),
    });
    res.status(r.status).type(r.headers.get("content-type") ?? "application/json");
    res.send(await r.text());
  } catch (err) {
    res.status(502).json({ error: "partner API unreachable", detail: err.cause?.code ?? err.name });
  }
});

app.listen(8080, () => console.log("relay on http://localhost:8080"));

Contra um substituto local da API parceira sem cabeçalhos CORS, a rota retornou o JSON com 200. Sirva a rota a partir da origem da página; a chave fica em uma variável de ambiente do servidor. Cabeçalhos, corpos e autenticação com fetch e Axios no lado do servidor estão explicados em cURL em JavaScript.

Mantenha o destino fixo. Uma rota que busca qualquer coisa que chegue em ?url= transforma o seu servidor em um proxy aberto que qualquer pessoa pode usar, inclusive contra endereços internos que só o seu servidor alcança. Os termos de uso e os limites de taxa da API continuam valendo: a chamada mudou de lugar, as regras não.

Por que proxies CORS públicos são um risco

Um proxy CORS público busca uma URL por você e adiciona Access-Control-Allow-Origin: * à resposta. O erro some, mas:

  • O operador vê a URL completa, todos os cabeçalhos (inclusive chaves de API e tokens) e a resposta.
  • Ele pode alterar a resposta, e a sua página vai confiar nela.
  • As requisições dos seus usuários passam por uma empresa com a qual você não tem nenhum acordo.
  • Os limites de taxa e as quedas dele passam a ser seus.

CORS e web scraping: por que o seu script em Python ou Node.js nunca vê esse erro

O CORS só existe em navegadores. Chamamos a mesma API que o navegador havia bloqueado, desta vez pelo Node.js 24:

js
// A mesma requisição pelo Node.js: sem navegador, sem verificação de CORS
const r = await fetch("http://localhost:3001/api/products");
console.log(r.status, r.headers.get("access-control-allow-origin"), await r.text());
text
200 null [{"id":1,"name":"Desk lamp"}]

Nenhum cabeçalho Access-Control-Allow-Origin, e o Node.js lê a resposta mesmo assim; o Requests do Python e o curl se comportam da mesma forma. Se você tenta coletar dados com fetch() no console do navegador ou em um app de front-end, o erro de CORS está dizendo que esse trabalho pertence ao código do lado do servidor. Qual linguagem combina com ele é o que comparamos em Extração de dados: JavaScript ou Python?. As regras do site continuam valendo ali: siga o robots.txt, mantenha o ritmo de requisições baixo e use a API oficial quando ela existir.

Um proxy não resolve um erro de CORS. O navegador compara a origem da página com o cabeçalho Access-Control-Allow-Origin, e o endereço IP não faz parte dessa verificação; então um proxy residencial, um proxy de datacenter ou uma VPN não mudam nada. Proxies pertencem ao cliente HTTP do seu código do lado do servidor, como mostramos em Como usar proxy no Node.js.

Para coletas permitidas que precisam de resultados locais em muitos países e cidades, os Proxies residenciais enviam as requisições por conexões residenciais, com segmentação por país e cidade.

Para grandes volumes de requisições a APIs públicas e páginas pouco protegidas, os Proxies de datacenter oferecem endereços IPv4 dedicados com tráfego sem cota.

Quem encontra erros de CORS?

  • Desenvolvedores front-end cujo app e cuja API rodam em portas diferentes.
  • Aplicações de página única (SPAs) que chamam uma API de terceiros direto do navegador.
  • Equipes que movem uma API para um subdomínio como api.example.com, ou para https.
  • Front-ends gerados por IA que chamam uma API diretamente, às vezes com a chave dentro da página.

Erros comuns

  • Adicionar Access-Control-Allow-Origin à requisição. É um cabeçalho de resposta; no fetch() ele vira um cabeçalho personalizado que dispara um preflight.
  • Usar mode: "no-cors". A resposta volta opaca: status 0, corpo ilegível.
  • Uma extensão "allow CORS" ou a segurança web desativada. Isso muda só o seu próprio navegador, e uma extensão assim pode ler as páginas em que roda.
  • Responder * enquanto o front-end envia cookies. O navegador rejeita essa combinação.
  • app.options("*", cors()) no Express 5. O app para na inicialização com PathError [TypeError]: Missing parameter name at index 1: *; app.use(cors()) já responde aos preflights.
  • Middleware de autenticação que rejeita OPTIONS. Preflights não levam token, então falham com "It does not have HTTP ok status".

Guia de decisão

SituaçãoO que fazer
Front-end e API em portas diferentes no desenvolvimentoProxy do servidor de desenvolvimento (server.proxy no Vite)
A sua API, chamada pelo seu front-endPermita as origens exatas; responda ao OPTIONS
O front-end envia cookiesOrigem exata, Access-Control-Allow-Credentials: true, Vary: Origin
Um cabeçalho ou método "is not allowed"Adicione em Access-Control-Allow-Headers ou -Methods
O erro aparece depois de um deploy ou sob cargaConfira o status real com curl; always no nginx
Uma API de terceiros sem cabeçalhos CORSChame pelo seu back-end, com a chave no servidor
Coletar dados de outros sitesCódigo no lado do servidor; ali o CORS não se aplica

Perguntas frequentes

Por que o Postman ou o curl funcionam enquanto o navegador mostra um erro de CORS?

Só navegadores aplicam o CORS. O servidor envia a mesma resposta para qualquer cliente, e só o navegador verifica Access-Control-Allow-Origin antes de entregá-la a um script. Um curl que funciona prova que a API está no ar, não que um navegador pode ler a resposta.

Por que recebo um erro de CORS no localhost?

Uma porta diferente é uma origem diferente: localhost:5173 e localhost:3000 são duas origens na mesma máquina. Permita a origem do front-end na API ou use o proxy do servidor de desenvolvimento. Além disso, desde o Chrome 142, um site público que chama localhost ou um dispositivo da sua rede doméstica precisa da sua permissão; se você recusar, o console informa um espaço de endereços loopback ou local negado.

O mode: "no-cors" resolve um erro de CORS?

Não. A requisição é enviada, mas o navegador devolve uma resposta opaca; no nosso teste o status foi 0, o tipo opaque e o corpo ilegível. Só serve para requisições cuja resposta você nunca vai ler.

O CORS é um recurso de segurança para a minha API?

Não. Ele impede que um site leia os dados de outro site com os cookies de um visitante, mas qualquer pessoa ainda pode chamar a sua API com um script. Proteja a API com autenticação, autorização e limites de taxa.

Um proxy ou uma VPN resolvem um erro de CORS?

Não. O navegador compara a origem da página com o cabeçalho Access-Control-Allow-Origin da resposta; o endereço IP não faz parte da verificação.

Por que só a minha requisição POST falha enquanto o GET funciona?

O POST provavelmente precisa de um preflight por causa de um corpo JSON, de um cabeçalho Authorization ou de um cabeçalho personalizado. Se a API não responde corretamente à requisição OPTIONS, o navegador nunca envia o POST. Procure a entrada OPTIONS na aba Network (Rede) ou envie o preflight com curl.

Em resumo

Um erro de CORS é o navegador se recusando a entregar ao seu script a resposta de outra origem; a requisição normalmente chegou ao servidor. Leia o motivo no console, confira o status real com curl e corrija o servidor: origens exatas, uma resposta ao OPTIONS, os métodos e cabeçalhos certos, Vary: Origin. Use o proxy do servidor de desenvolvimento enquanto desenvolve e o seu próprio back-end para APIs que você não controla. A coleta de dados pertence ao código do lado do servidor, onde o CORS não se aplica; os tipos de proxy para esse trabalho estão na nossa página de serviços de proxy.