Warum ein Proxy-Request keine Versuche mehr übrig hat

Das Problem

Ein POST /api/proxy/-Aufruf liefert einen Fehler und keine Daten zurück. Die Meldung ist kurz und hat immer dieselbe Struktur:

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

Dieser Satz liest sich genau gleich, egal ob jeder Exit blockiert war, jeder Exit tot war oder FourA die echte Seite bei fast jedem Versuch abgerufen hat und deine eigenen validate-Regeln sie verworfen haben. Diese drei Fälle erfordern jeweils völlig andere Lösungen.

Die Antwort: attemptReport

Jede fehlgeschlagene Proxy-Response enthält neben dem Fehler ein attemptReport-Objekt. Es zählt, worauf die Versuche tatsächlich gestoßen sind:

{
  "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
}

Der error-String bleibt bewusst unverändert, damit ein Client, der darauf matcht, weiterhin funktioniert. Lies attemptReport.summary für eine einzeilige Antwort oder die Zähler, wenn du darauf verzweigen möchtest.

Fields

Field Type What it counts
total integer Unternommene Versuche
noResponse integer Der Exit hat nie geantwortet, die Site wurde also nie erreicht
defense integer Die Site hat geantwortet und ein Bot-Check-Anbieter wurde in dieser Antwort erkannt
contentRejected integer HTTP 200, kein Bot-Check, nur durch dein validate.data abgelehnt
statusRejected integer Die Site hat geantwortet, kein Bot-Check, durch dein validate.status abgelehnt
other integer Geantwortet, und keines der obigen Kriterien
vendors string[] Jeder Bot-Check-Anbieter, der irgendwo im Task erkannt wurde
profilesTried string[] Die vom Task gesendeten Browser-Profile in der Reihenfolge der ersten Verwendung. default bedeutet, dass der Request genau so gesendet wurde, wie du ihn geschrieben hast.
summary string Ein aus den Zählern erstellter Satz. Sicher zum Loggen oder für die Anzeige für Nutzer.

Reading It

contentRejected is high

Die Seiten sind angekommen. Deine validate.data-Regel hat nicht darauf gematcht.

Das kannst du selbst beheben, und es ist der Fall, den jedes andere Signal verbirgt: Die Requests sehen in jeder Metrik wie Fehler aus, obwohl FourA die ganze Zeit echte Inhalte geliefert hat. Rufe die Seite einmal über POST /api/single/ ganz ohne validate ab, sieh dir an, was tatsächlich zurückkommt, und schreibe die Regel passend dazu um.

Eine häufige Ursache ist eine einzelne Regel für mehrere Seiten, die nicht identisch aufgebaut sind. Ein Selektor, der auf Artikelseiten existiert, aber nicht auf Videoseiten, schlägt jedes Mal fehl, wenn er auf einer Videoseite landet, dauerhaft und bei vollen Kosten.

statusRejected is high

Die Site hat geantwortet und deine validate.status-Regel hat abgelehnt. Wenn diese Statuscodes 401, 403, 429 oder 503 sind, weist die Site den Client ab, anstatt zu melden, dass die Seite nicht existiert. Versuche:

  • Ein anderes Browser-Profil (browser, os, version im inneren request-Objekt)
  • exitCountries, falls der Inhalt regional gesperrt ist
  • POST /api/browser/, falls die Ablehnung JavaScript zur Freigabe erfordert

defense is high

Ein Bot-Check wurde in den Antworten erkannt, und vendors benennt den Anbieter. Siehe Anti-Bot Defenses dafür, was FourA derzeit umgeht und was es nur meldet. Wenn der Anbieter an diesem Endpoint nicht umgangen wird, verlagere den Aufruf auf POST /api/browser/ oder POST /api/auto/.

noResponse is high

Die Exits haben überhaupt nicht geantwortet, daher liegen keine Erkenntnisse über das Ziel vor. Erhöhe maxTries, erhöhe timeout_ms und prüfe, ob die URL aus dem öffentlichen Internet auflösbar ist.

other is high

Geantwortet und in keine der obigen Kategorien eingestuft. Prüfe total_time im Vergleich zu deinem timeout_ms: Ein Ziel, das langsamer als dein Budget ist, landet hier.

Browser Profile Rotation

Wenn eine Website den von FourA gesendeten Browser ablehnt, beharrt Proxy nicht darauf, sondern versucht es mit einer anderen Familie aus dem öffentlichen Profilkatalog. Das kostet keinen zusätzlichen Versuch: Die Rotation ändert, was ein Retry sendet, nicht, ob einer stattfindet.

profilesTried zeigt dir, wie das abläuft. Ein Eintrag bedeutet, dass der Request jedes Mal wie vorgegeben gesendet wurde. Mehrere Einträge bedeuten, dass die Rotation lief und die Website jeden davon abgelehnt hat. Das ist ein anderer Fall, als wenn überhaupt keine Rotation stattgefunden hätte.

Bei einer erfolgreichen Proxy-Response erscheint ein profile-Feld nur dann, wenn die Rotation einen Browser gewählt hat, den du nicht angefordert hast:

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

Der Wert ist eine Katalog-ID aus GET /api/profiles. Fehlt er, wurde der Request exakt wie angegeben gesendet. Ist er vorhanden, war der funktionierende Browser nicht der von dir eingegebene. Übergib diese ID bei Folgeaufrufen als profile, anstatt den fehlgeschlagenen erneut zu senden. Das Dashboard Playground erledigt das mit Carry automatisch für dich.

Ein explizites profile, browser, os oder version in deinem Request wird niemals überschrieben. Dasselbe gilt für Requests mit deinem eigenen User-Agent- oder Cookie-Header, da eine Clearance an die Signatur gebunden ist, die sie erhalten hat.

Im Code auslesen

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", [])))

Verwandte Themen

Aktualisiert: 31. August 2026