Por que uma requisição proxy esgotou as tentativas

O Problema

Uma chamada POST /api/proxy/ retorna com um erro e sem dados. A mensagem é curta e sempre no mesmo formato:

{
  "error": "Download maxTry limit reached",
  "total": 34.812,
  "request": { "...": "..." }
}

Essa frase tem a mesma leitura se todas as saídas foram bloqueadas, se todas as saídas estavam inativas ou se a FourA buscou a página real em quase todas as tentativas e as suas próprias regras validate a descartaram. Esses três casos exigem correções opostas.

A resposta: attemptReport

Cada resposta de Proxy com falha traz um objeto attemptReport ao lado do erro. Ele contabiliza o que as tentativas realmente encontraram:

{
  "error": "Download maxTry limit reached",
  "attemptReport": {
    "total": 25,
    "noResponse": 0,
    "defense": 0,
    "contentRejected": 25,
    "statusRejected": 0,
    "other": 0,
    "vendors": [],
    "profilesTried": ["default"],
    "summary": "25 attempt(s): 25 returned HTTP 200 with no defense present and were rejected only by your validate.data - the page was fetched, your content rule did not match it"
  },
  "total": 34.812
}

A string error é mantida deliberadamente inalterada, para que um cliente que faça correspondência com ela continue funcionando. Leia attemptReport.summary para uma resposta de uma linha, ou as contagens quando quiser criar ramificações com base nelas.

Campos

Campo Tipo O que conta
total integer Tentativas realizadas
noResponse integer A saída nunca respondeu, portanto o site nunca foi alcançado
defense integer O site respondeu e um fornecedor de verificação de bot foi reconhecido nessa resposta
contentRejected integer HTTP 200, sem verificação de bot, rejeitado apenas pelo seu validate.data
statusRejected integer O site respondeu, sem verificação de bot, rejeitado pelo seu validate.status
other integer Respondeu, e nenhum dos itens acima
vendors string[] Todos os fornecedores de verificação de bot reconhecidos em qualquer lugar da tarefa
profilesTried string[] Os perfis de navegador enviados pela tarefa, na ordem de primeiro uso. default significa que a solicitação foi enviada exatamente como você a escreveu.
summary string Uma frase construída a partir das contagens. Seguro para registrar em logs ou exibir a um usuário.

Como interpretar

contentRejected está alto

As páginas chegaram. Sua regra validate.data não correspondeu a elas.

Este é o caso que você mesmo pode corrigir, e é o que todos os outros sinais ocultam: as solicitações parecem falhas em todas as métricas, mas o FourA estava entregando conteúdo real o tempo todo. Busque a página uma vez via POST /api/single/ sem nenhum validate, veja o que realmente retorna e reescreva a regra com base nisso.

Uma causa comum é uma única regra aplicada a um conjunto de páginas que não são todas iguais. Um seletor que existe em páginas de artigos e não em páginas de vídeos falhará toda vez que atingir uma página de vídeo, indefinidamente, com custo total.

statusRejected está alto

O site respondeu e sua regra validate.status recusou. Se esses status forem 401, 403, 429 ou 503, o site está recusando o cliente em vez de informar que a página não existe. Tente:

  • Outro perfil de navegador (browser, os, version no objeto request interno)
  • exitCountries se o conteúdo tiver bloqueio regional
  • POST /api/browser/ se a recusa exigir JavaScript para ser liberada

defense está alto

Uma verificação de bot foi reconhecida nas respostas, e vendors indica qual. Consulte Defesas Anti-Bot para saber o que o FourA resolve atualmente e o que ele apenas relata. Se o fornecedor não for um dos que são resolvidos neste endpoint, migre a chamada para POST /api/browser/ ou POST /api/auto/.

noResponse está alto

As saídas não responderam, portanto nada foi identificado sobre o destino. Aumente maxTries, aumente timeout_ms e verifique se a URL resolve a partir da internet pública.

other está alto

Respondeu, e não foi classificado em nenhum dos casos acima. Verifique total_time em relação ao seu timeout_ms: um destino mais lento do que o seu orçamento cairá aqui.

Rotação de perfis de navegador

Quando um site recusa o navegador enviado pela FourA, o Proxy para de insistir nele e tenta outra família do catálogo de perfis públicos. Isso não consome tentativas adicionais: a rotação altera o que uma nova tentativa envia, nunca se ela acontece ou não.

profilesTried é como você vê isso acontecer. Uma única entrada significa que a request foi enviada exatamente como definida todas as vezes. Várias entradas indicam que a rotação foi executada e o site recusou cada uma delas, o que é uma situação diferente de não ter rotacionado nenhuma vez.

Em uma response de Proxy bem-sucedida, o campo profile aparece apenas quando a rotação escolheu um navegador que você não solicitou:

{
  "status": 200,
  "data": "<!doctype html>...",
  "proxy": "A1B2C3",
  "profile": "...",
  "total": 4.108
}

O valor é um id de catálogo de GET /api/profiles. Ausente significa que a request foi enviada exatamente como escrita. Presente significa que o navegador que funcionou não foi o que você digitou, portanto passe esse id de volta como profile nas chamadas seguintes em vez de repetir o que falhou. O dashboard Playground faz isso para você com Carry.

Um profile, browser, os ou version explícito na sua request nunca é sobrescrito. Nem uma request que contenha seu próprio header User-Agent ou Cookie, já que uma liberação está vinculada à assinatura que a obteve.

Lendo isso no código

import requests

API = "https://eu.api.foura.ai"
H = {"X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json"}

r = requests.post(f"{API}/api/proxy/", headers=H, json={
    "maxTries": 5,
    "request": {
        "method": "GET",
        "url": "https://example.com/product/42",
        "validate": {"data": {"accept": ["Add to cart"]}},
    },
}).json()

if "error" in r:
    rep = r.get("attemptReport", {})
    print(rep.get("summary", r["error"]))

    if rep.get("contentRejected", 0) > rep.get("total", 0) / 2:
        # The pages arrived. The validate rule is what threw them away.
        raise SystemExit("validate.data did not match the real page")
    if rep.get("defense", 0):
        print("bot check met:", ", ".join(rep.get("vendors", [])))

Relacionados

Atualizado em: 31 de agosto de 2026