3

Pitch: API de geolocalização devolvendo 429 só em produção: o que aconteceu no Cloudflare e como resolvi

O primeiro sinal foi um 429 Too Many Requests. Não era um erro isolado: toda chamada à API de geolocalização saindo do Cloudflare voltava 429. Todas.

Eu construí o MyIPScan, um conjunto de ferramentas online de rede e privacidade. Uma delas consulta a localização aproximada de um IP qualquer. Diferente das ferramentas que mostram o seu próprio IP, essa consulta roda no servidor, em Cloudflare Pages Functions (Workers por baixo).

E foi justamente essa parte que quebrou.

O que estava em jogo

A ferramenta usava três APIs gratuitas em cadeia: ipapi.co primeiro, ipwho.is e ipwhois.app como fallback. A ideia era simples: se uma falhar, a próxima responde.

Só que as três respondiam 429 para tudo que vinha do Cloudflare. O fallback não salvava nada, só repetia a mesma falha três vezes. Na prática, a busca pela localização de outro IP ficava sem resposta útil, porque não sobrava nenhum provedor funcionando.

Por que o teste local não pegou

Rodando a mesma chamada no meu notebook, as três APIs respondiam normalmente. Do notebook tudo parecia certo. Do servidor, nada funcionava.

Não era "às vezes 429", que lembraria um limite de uso comum. Era 429 em toda chamada a partir da produção e resposta normal a partir do notebook. Essa diferença era a pista.

A causa

O mais provável é o seguinte: quando seu código roda num Worker, a requisição sai por IPs do Cloudflare, e esses IPs são compartilhados com muita gente. APIs gratuitas costumam limitar por IP de origem. Se muitos projetos batem na mesma API saindo pelos mesmos endereços, o limite do plano grátis pode estourar antes da sua primeira chamada.

Não achei documentação de limite para ipwho.is e ipwhois.app, então não vou chutar números. O ipapi.co é explícito no FAQ: o plano grátis dá "up-to 1000 IP lookups in a day", "is not meant for use in production or deployments" e "may use heuristics to further restrict free tier API calls under certain conditions" (ipapi.co). Ou seja: o próprio provedor avisa que produção não é o caso de uso do plano grátis.

No notebook, o IP de saída era o da minha conexão, sozinho. Por isso o teste local passava. Ele estava testando outra coisa.

A correção

Troquei para o RIPEstat Data API, do RIPE NCC, usando o endpoint maxmind-geo-lite, que "returns geolocation information for the given IP space based on MaxMind's GeoLite2 data source".

Segundo a documentação, não há limite no número de requisições, mas eles pedem registro para quem faz regularmente mais de 1.000 por dia e limitam a 8 requisições simultâneas por IP. Existe também o parâmetro opcional sourceapp para você se identificar.

Um exemplo do formato, tratando erro de forma explícita:

async function geoLookup(ip) {
  const url =
    "https://stat.ripe.net/data/maxmind-geo-lite/data.json" +
    `?resource=${encodeURIComponent(ip)}&sourceapp=seu-projeto`;

  const res = await fetch(url, { headers: { accept: "application/json" } });

  if (!res.ok) {
    // Nada de falhar em silêncio: registra status e Retry-After
    console.error("geo lookup falhou", {
      status: res.status,
      retryAfter: res.headers.get("retry-after"),
    });
    return { ok: false, status: res.status };
  }

  const body = await res.json();
  const loc = body?.data?.located_resources?.[0]?.locations?.[0];
  if (!loc) return { ok: false, status: res.status, reason: "sem dados" };

  return {
    ok: true,
    country: loc.country, // código ISO, ex.: "US"
    city: loc.city || null, // pode vir vazio, como no 8.8.8.8
    lat: loc.latitude,
    lon: loc.longitude,
  };
}

Dois detalhes importam aqui. Primeiro, res.ok é checado antes de qualquer res.json(): um 429 não vira um objeto vazio que segue pelo código como se fosse resposta válida. Segundo, o log guarda o status e o Retry-After, que segundo a MDN pode vir junto com o 429 dizendo quanto tempo esperar (o código é definido na RFC 6585, seção 4).

Hoje a resposta da própria API do site diz de onde veio o dado: o campo providers traz "RIPEstat (MaxMind GeoLite)" com status "ok". Expor a fonte e o status na resposta deixa qualquer falha futura visível na hora, sem precisar adivinhar.

Como testar uma API de terceiros a partir da produção

  • Faça a primeira chamada de teste do mesmo ambiente onde o código vai rodar (Worker, Lambda, container), não da sua máquina.
  • Registre o status HTTP de toda chamada externa, inclusive os de sucesso, pelo menos no início.
  • Trate 429 como caso próprio: logue Retry-After e não deixe virar resposta vazia.
  • Antes de montar fallback, confirme que os provedores do fallback não falham pelo mesmo motivo.
  • Leia o FAQ do plano grátis procurando "production", "per IP" e "heuristics".
  • Prefira APIs que documentam limite e política de uso de forma explícita.
  • Monitore taxa de erro, não só se o endpoint "está no ar".

A regra que fica

Teste APIs de terceiros a partir da produção, não do notebook. O seu IP de casa e o IP de saída de uma nuvem compartilhada são dois clientes diferentes para quem está do outro lado, e podem receber respostas diferentes.

E se você já tomou 429 só em produção com outra API, conta nos comentários qual era. Fico curiosa para saber se o padrão se repete.

Carregando publicação patrocinada...
1

O limite de 8 requisições simultâneas do RIPEstat também é por IP, e o IP de saída do Worker continua sendo o mesmo compartilhado com todo mundo que roda lá. É bem mais folgado que o dos planos grátis, mas é o mesmo tipo de risco. Guardar o resultado por IP no Cache API (caches.default) por algumas horas já corta muita chamada repetida, até porque a localização de um IP quase não muda de um dia pro outro. Só lembrando que esse cache é por data center.

E nas ferramentas que mostram o IP do próprio visitante nem precisa de API externa, o request.cf já traz país, cidade, latitude e longitude de quem fez a requisição.