Erro de rede ao chamar a API de IA no navegador: é CORS, chave exposta ou timeout do deploy?

O erro de CORS na API de IA quase sempre vem de um diagnóstico trocado: o mesmo Failed to fetch no console pode ser CORS, SDK barrando chave exposta ou timeout do deploy. A OpenAI não devolve Access-Control-Allow-Origin para origem de navegador, enquanto Anthropic, Google e OpenRouter suportam CORS (a Anthropic exige o header anthropic-dangerous-direct-browser-access). Os SDKs JS da OpenAI e da Anthropic bloqueiam browser por padrão e só liberam com dangerouslyAllowBrowser: true, o que expõe sua credencial. A correção que resolve tudo de uma vez é um proxy no backend, com a chave em variável de ambiente no servidor.
Fala aí, beleza? Você plugou o modelo na sua aplicação web, rodou no navegador e o console cuspiu um Failed to fetch sequinho, sem nenhuma pista do que quebrou
Aí começa a caça ao culpado: é o CORS? é o SDK barrando a chave que ficou exposta no front? é o deploy cortando a resposta no meio?
O chato é que os três problemas dão MUITO parecido na tela: a requisição sai, nada útil volta, e o console fala de "network error" como se fosse a internet do usuário
Só que em integração via API essas são três causas diferentes, com três correções diferentes
Bora separar os três, na ordem em que eles aparecem no console
Erro de CORS: a API bloqueia a chamada que sai do navegador
O sintoma: a mensagem vem redondinha no console, e ela é a sua melhor pista
Com a OpenAI, algo como blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource
Com a Anthropic, a mensagem é outra: CORS requests must set 'anthropic-dangerous-direct-browser-access' header
A causa: a API da OpenAI não envia o cabeçalho Access-Control-Allow-Origin para origens de navegador, então o fetch direto do front-end é barrado pela política de CORS do próprio browser
Cobertura técnica de agosto de 2026 confirma que chamar a OpenAI direto do JavaScript do navegador continua inviável, enquanto Anthropic, Google e OpenRouter suportam CORS
Ou seja: não é bug seu, é comportamento do provedor
No caso da Anthropic o buraco é outro. Para bater direto em https://api.anthropic.com/v1/messages a partir do navegador, é preciso mandar o cabeçalho anthropic-dangerous-direct-browser-access com valor true. Sem ele, a chamada falha com aquela mensagem explícita
Formação Claude Code
Domine Claude Code do absoluto zero até o avançado
- 118 aulas
- 4 projetos
- 9h 33min
Repara no nome do header, viu? Tem "dangerous" ali no meio de propósito 😅
A solução: mover a chamada para o servidor e fazer o front conversar só com a sua própria origem. Isso mata o CORS na raiz, porque a requisição passa a ser mesma origem
Se o seu cenário for um daqueles casos em que o browser faz sentido (falo deles daqui a pouco), aí sim você envia o header exigido pelo provedor
Como prevenir: trate o front-end como ambiente público por definição. Tudo que chega no bundle do cliente é legível, inspecionável e reutilizável por qualquer pessoa com o DevTools aberto
"It looks like you’re running in a browser-like environment": o SDK barrando a chave exposta
O sintoma: você nem chega no CORS. O SDK JavaScript oficial da OpenAI lança um erro que começa exatamente com It looks like you're running in a browser-like environment
O SDK TypeScript da Anthropic faz o mesmo: se recusa a rodar
A causa: os dois desabilitam o uso em navegador por padrão. Não é acidente, é guardrail (limite) proposital
A solução técnica, e por que ela costuma ser a errada: existe uma flag pra destravar, a mesma nos dois SDKs
// SDK TypeScript da Anthropic
const client = new Anthropic({
apiKey: null,
dangerouslyAllowBrowser: true
})
Funciona? Funciona. Mas a própria documentação do SDK da Anthropic descreve o preço disso: habilitar a opção expõe as credenciais secretas no código do lado do cliente, e qualquer usuário com acesso ao navegador pode inspecionar, extrair e usar essas credenciais
A OpenAI bate na mesma tecla pelo lado da conta: chave exposta em navegador ou app móvel permite que usuários mal-intencionados façam requisições em seu nome, gerando cobranças inesperadas
Traduzindo: você não vai ver um erro. Você vai ver a fatura
Como prevenir: a recomendação explícita da OpenAI é rotear as requisições pelo próprio backend, com a chave carregada de variável de ambiente ou de um serviço de gerenciamento de chaves no servidor
Vale o mesmo cuidado que se toma ao não colar a credencial no chat quando você pede ajuda pra IA com um trecho de integração: chave é chave, ela só existe em lugar controlado
E tem uma camada extra que pouca gente liga: a OpenAI oferece IP allowlisting. Com ele habilitado, só requisições vindas dos IPs ou faixas configuradas são aceitas, e as demais são rejeitadas mesmo com chave válida
Se a sua chave só sai do IP do seu servidor, uma chave vazada vale muito menos pro atacante
Quando chamar direto do navegador é aceitável (e quando não é)
Não é proibido, é situacional. A documentação do SDK da Anthropic lista os casos de uso legítimos do modo browser:
- Ferramentas internas com usuários confiáveis, aquele dashboard que só o time acessa
- Desenvolvimento e depuração com credenciais de vida curta, pra prototipar rápido sem montar servidor
- Bring your own API key (BYOK), o padrão em que o usuário final fornece a própria chave, e o risco é dele, não seu
Repara no que NÃO está nessa lista: aplicação pública rodando com a sua chave
Se é produto aberto pra internet e a credencial é sua, o navegador está fora de cogitação. Simples assim
Como montar um proxy mínimo no backend para a API de IA
Essa é a correção que resolve CORS e exposição de chave de uma tacada só. A ideia é boba de tão simples: o front fala com você, você fala com o provedor
Se você já montou um servidor que repassa requisição pra outro serviço, é exatamente isso, sem mistério
- Crie uma rota no seu servidor que o front vai chamar. Ela recebe só o payload que o usuário mandou (mensagem, histórico, parâmetros), nunca a chave
// POST /api/chat (o front chama SÓ isso, na mesma origem)
export async function POST(req) {
const payload = await req.json()
// ...
}
O erro comum deste passo: continuar importando o SDK do provedor no código que vai pro bundle do cliente. Se ele está no front, a chave está no front, e a flag dangerouslyAllowBrowser vai só calar o aviso, não resolver o problema
- Carregue a chave de uma variável de ambiente do servidor. Ela nunca aparece em resposta, log ou payload devolvido pro browser
const apiKey = process.env.AI_API_KEY
if (!apiKey) throw new Error('chave ausente no ambiente do servidor')
O erro comum deste passo: usar uma variável que o framework injeta no build do front (aqueles prefixos públicos tipo NEXT_PUBLIC_ e afins). Nesse caso a chave é literalmente compilada dentro do JavaScript que o usuário baixa. Já vi gente descobrir isso do jeito ruim 🙃
Se a sua stack roda em container, o raciocínio é o mesmo de usar secrets no Docker em vez de variável solta: a credencial vive no ambiente do servidor, não no artefato que você distribui
- Chame o endpoint do provedor a partir do servidor, montando os headers de autenticação que a documentação dele exige
const upstream = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'content-type': 'application/json',
...authHeaders(apiKey) // headers de auth conforme a doc do provedor
},
body: JSON.stringify(payload)
})
O erro comum deste passo: copiar pro servidor o header anthropic-dangerous-direct-browser-access que você tinha colocado no front. Ele existe pra destravar chamada vinda de navegador, e no backend não faz sentido nenhum
- Devolva a resposta pro front na sua própria origem. Sem CORS envolvido, porque agora é mesma origem
- Se a resposta é em streaming, repasse o corpo sem bufferizar, mantendo o content type de eventos
return new Response(upstream.body, {
headers: {
'content-type': 'text/event-stream',
'cache-control': 'no-cache'
}
})
O erro comum deste passo: dar await upstream.json() na resposta em stream. Você espera o texto inteiro terminar pra só então mandar tudo de uma vez, e perde exatamente o efeito que motivou usar streaming
- Valide o payload antes de repassar. Sua rota virou uma API pública: qualquer pessoa pode chamar ela com o que quiser, e quem paga a conta é você
O erro comum deste passo: achar que o proxy resolveu segurança. Ele protegeu a chave, mas abusar do seu endpoint continua sendo possível se não houver autenticação e limite de uso
Stream que corta no meio: conexão ociosa, EventSource e retomada
Proxy no ar, CORS resolvido, e mesmo assim a resposta começa a chegar, anda um pouco e para. Ou a requisição simplesmente falha sem resposta nenhuma
Causa 1: conexão ociosa. A documentação da Anthropic registra que algumas redes derrubam conexões ociosas após um período variável, o que faz a requisição falhar ou dar timeout sem resposta
É por isso que aquele modo sem streaming em tarefa longa é traiçoeiro: fica muito tempo sem trafegar byte nenhum
Causa 2: você está usando EventSource. A API EventSource do navegador só faz requisições GET, não aceita cabeçalhos customizados nem corpo de requisição
Ou seja, ela não serve pra mandar seu payload de chat via POST, ponto
E tem o efeito colateral: por padrão, quando a conexão entre cliente e servidor fecha, o EventSource reinicia a conexão automaticamente. O campo retry do protocolo SSE define em milissegundos quanto o navegador espera antes de tentar reconectar (valor não inteiro é ignorado)
Reconectar não é continuar. Ele começa de novo, e pro usuário parece que a IA travou e repetiu tudo
A solução, parte 1: trate os eventos direito. O streaming da Claude API usa server-sent events com uma sequência definida de eventos nomeados:
message_startcontent_block_startcontent_block_deltacontent_block_stopmessage_deltamessage_stop- mais
pingeerror
A documentação orienta que o seu código trate tipos de evento desconhecidos sem quebrar, porque novos tipos podem ser adicionados
Esse é um clássico: o parser dá throw num evento que ele não conhece, e o front interpreta como queda de rede
A solução, parte 2: retomada. A documentação da Anthropic descreve como retomar um stream interrompido: montar uma nova requisição com uma mensagem de usuário contendo a resposta parcial já recebida e a instrução para continuar
Como prevenir: para requisições longas, a recomendação é usar a Messages API em streaming ou a Message Batches API, que permite consultar o resultado por polling em vez de exigir conexão ininterrupta
E tem um sinal bem direto disso no SDK Python: ele lança ValueError quando uma requisição sem streaming deve passar de aproximadamente 10 minutos
O corte veio do deploy: limites de duração na Vercel e na Cloudflare
O sintoma: funciona liso no localhost e corta em produção, sempre mais ou menos no mesmo ponto
Quando o corte tem hora marcada, o suspeito é a plataforma
Os números da Vercel: no plano Hobby, com fluid compute, 300 segundos (5 minutos) é ao mesmo tempo o padrão e o máximo de duração de função, e durações estendidas não estão disponíveis nesse plano
Para times Pro e Enterprise, o máximo de 800 segundos está em disponibilidade geral com fluid compute, e existe um máximo estendido de 1800 segundos (30 minutos) em beta para runtimes suportados de Node.js, Bun e Python
A configuração, com App Router do Next.js e runtime Node.js ou Bun suportado, sai exportando maxDuration no arquivo da rota:
// app/api/chat/route.js
export const maxDuration = 1800
Fora do App Router, a configuração é feita por caminho de função no vercel.json
Os números da Cloudflare: o limite padrão de tempo de CPU de um Worker é 30 segundos (30000 ms) e, no plano Workers Paid, pode ser elevado até 5 minutos (300000 ms). Esse teto maior foi anunciado no changelog oficial da Cloudflare
O ajuste vai no arquivo de configuração do Wrangler:
{
"limits": { "cpu_ms": 300000 }
}
E quando o Worker estoura o limite de tempo de CPU, a Cloudflare retorna ao cliente o Error 1102 com a mensagem de que o Worker excedeu os limites de recurso. Se você viu esse número, já sabe de onde veio o corte
O alívio importante: nas duas plataformas, esperar não é o mesmo que trabalhar
No fluid compute da Vercel, o Active CPU cobra o tempo em que o código consome CPU de fato, e o tempo esperando I/O, como chamadas a modelos de IA ou banco de dados, não conta como Active CPU
Nos Workers é parecido: o tempo de wall-clock é ilimitado, justamente pro caso de espera por recursos lentos, e o tempo de CPU só conta a execução ativa do código. Esperar fetch, ler KV ou consultar banco não entra na conta
Então o gargalo raramente é ficar esperando o modelo responder. Costuma ser o teto de duração da função mesmo, ou processamento pesado que você colocou no meio do caminho
CORS e limites de execução: OpenAI, Anthropic, Vercel e Cloudflare lado a lado
Tabela de consulta rápida, pra quando você quer só o dado e voltar pro código
| Provedor | CORS para chamada do navegador | O que o SDK/API exige |
|---|---|---|
| OpenAI | Não envia Access-Control-Allow-Origin para origem de navegador (confirmado em ago/2026) |
SDK JS bloqueia browser por padrão; dangerouslyAllowBrowser: true pra destravar |
| Anthropic | Suporta CORS | Header anthropic-dangerous-direct-browser-access: true em https://api.anthropic.com/v1/messages; SDK TS exige dangerouslyAllowBrowser: true |
| Suporta CORS | não coberto nesta pesquisa | |
| OpenRouter | Suporta CORS | não coberto nesta pesquisa |
| Plataforma / plano | Teto de execução | Onde se configura |
|---|---|---|
| Vercel Hobby (fluid compute) | 300 segundos, padrão e máximo, sem duração estendida | maxDuration na rota do App Router ou por função no vercel.json |
| Vercel Pro e Enterprise (fluid compute) | 800 segundos em GA; até 1800 segundos em beta (Node.js, Bun, Python) | maxDuration na rota do App Router ou por função no vercel.json |
| Cloudflare Workers (padrão) | 30000 ms de CPU; wall-clock ilimitado | bloco limits com cpu_ms no Wrangler |
| Cloudflare Workers Paid | até 300000 ms de CPU; estouro devolve Error 1102 |
bloco limits com cpu_ms no Wrangler |
Conclusão
O diagnóstico é chato só até você olhar pra mensagem certa, e ela quase sempre está no console
Falou em Access-Control-Allow-Origin ou em anthropic-dangerous-direct-browser-access? é CORS, a chamada está saindo do navegador e não devia
Começou com It looks like you're running in a browser-like environment? é o SDK te protegendo de publicar a própria chave, e a flag que destrava isso não é solução, é assinatura de risco
A resposta começou e morreu no meio? aí a briga é entre conexão ociosa, EventSource reconectando em vez de continuar, e o teto de duração da plataforma. Error 1102 já entrega o culpado de bandeja
O próximo passo prático é sempre o mesmo, e ele resolve os dois primeiros de uma vez: sobe o proxy no backend, guarda a chave em variável de ambiente no servidor (com IP allowlisting se o provedor oferecer) e configura o teto de duração da plataforma ANTES de publicar
até o próximo post! 😄
Perguntas frequentes
Por que a resposta da API de IA trava ou some depois de alguns segundos quando o backend está na Vercel?
Porque cada plano tem um teto de duração de função. No plano Hobby com fluid compute, 300 segundos é ao mesmo tempo o padrão e o máximo, sem duração estendida disponível. Em times Pro e Enterprise o máximo em disponibilidade geral sobe para 800 segundos, com um teto estendido de até 1800 segundos em beta para runtimes suportados de Node.js, Bun e Python.
Como aumentar o tempo máximo de execução de uma rota que chama a API de IA na Vercel?
Com App Router do Next.js em runtime Node.js ou Bun suportado, basta exportar maxDuration no arquivo da rota, por exemplo export const maxDuration = 1800. Fora do App Router, a configuração equivalente é feita por caminho de função no vercel.json.
O que é o erro 1102 ‘Worker exceeded resource limits’ no Cloudflare Workers?
É o erro que a Cloudflare devolve ao cliente quando o Worker estoura o limite de tempo de CPU configurado. O padrão é 30000 ms (30 segundos), e no plano Workers Paid esse teto pode ser elevado até 300000 ms (5 minutos), configurado no bloco limits do arquivo do Wrangler.
Por que uma chamada de IA no Cloudflare Workers não estoura o limite mesmo demorando muito para responder?
Porque o limite do Worker é de tempo de CPU, não de tempo total de espera (wall-clock). O wall-clock é ilimitado justamente para cobrir espera por recursos lentos, e tempo parado esperando fetch, leitura de KV ou consulta a banco não entra na conta de CPU.
Dá para usar o EventSource do navegador para consumir o streaming da API da Anthropic direto?
Na prática não funciona bem, porque a API EventSource do navegador só faz requisições GET, sem aceitar cabeçalhos customizados nem corpo de requisição. Como autenticação e o header exigido para acesso via browser dependem de cabeçalho customizado, o EventSource puro fica limitado para esse cenário.
Como retomar uma resposta de IA que parou no meio do streaming por causa de timeout?
A documentação da Anthropic orienta montar uma nova requisição de continuação, enviando como mensagem de usuário o conteúdo parcial já recebido junto com a instrução para continuar de onde parou. Isso evita reprocessar tudo do zero quando a conexão cai antes do fim.
Formações
Formação Vibe Coding
Do Prompt ao Produto: Crie Software Real com IA
- 474 aulas
- 20 projetos
- 39h 27min
Blog | Mais populares
O que significa “ChatGPT network error” e como resolver
O “ChatGPT Network Error” é uma ocorrência frequente na rotina de muitos usuários do ChatGPT. Porém, poucos compreendem seu significado, quando esse erro surge, etc. […]

Checklist de segurança n8n VPS pública: guia essencial para proteger sua instalação
Checklist de segurança n8n VPS pública: guia essencial para proteger sua instalação A popularidade da automação de processos com o n8n está em alta, principalmente […]

Como usar o Antigravity do Google: guia completo do zero ao primeiro app
Aprenda neste guia prático como usar o Antigravity do Google: descubra a instalação, configuração, criação de projetos com o Agent Manager e o primeiro deploy, […]
