Why a Proxy Request Ran Out of Tries

The Problem

A POST /api/proxy/ call comes back with an error and no data. The message is short and always the same shape:

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

That sentence reads identically whether every exit was blocked, every exit was dead, or FourA fetched the real page on almost every attempt and your own validate rules threw it away. Those three need opposite fixes.

The Answer: attemptReport

Every failed Proxy response carries an attemptReport object beside the error. It counts what the attempts actually ran into:

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

The error string is deliberately unchanged, so a client matching on it keeps working. Read attemptReport.summary for a one-line answer, or the counts when you want to branch on them.

Fields

Field Type What it counts
total integer Attempts made
noResponse integer The exit never answered, so the site was never reached
defense integer The site answered and a bot-check vendor was recognised on that answer
contentRejected integer HTTP 200, no bot check, rejected only by your validate.data
statusRejected integer The site answered, no bot check, rejected by your validate.status
other integer Answered, and none of the above
vendors string[] Every bot-check vendor recognised anywhere in the task
profilesTried string[] The browser profiles the task sent, in first-use order. default means the request went out exactly as you wrote it.
summary string One sentence built from the counts. Safe to log or show a user.

Reading It

contentRejected is high

The pages arrived. Your validate.data rule didn't match them.

This is the one you can fix yourself, and it's the one every other signal hides: the requests look like failures in every metric, and FourA was delivering real content the whole time. Fetch the page once through POST /api/single/ with no validate at all, look at what actually comes back, and rewrite the rule against it.

A common cause is one rule applied to a set of pages that aren't all the same. A selector that exists on article pages and not on video pages fails every time it lands on a video page, forever, at full cost.

statusRejected is high

The site answered and your validate.status rule said no. If those statuses are 401, 403, 429, or 503, the site is refusing the client rather than saying the page isn't there. Try:

  • Another browser profile (browser, os, version on the inner request object)
  • exitCountries if the content is region-locked
  • POST /api/browser/ if the refusal needs JavaScript to clear

defense is high

A bot check was recognised on the answers, and vendors names which. See Anti-Bot Defenses for what FourA clears today and what it only reports. If the vendor isn't one that gets cleared on this endpoint, move the call to POST /api/browser/ or POST /api/auto/.

noResponse is high

The exits didn't answer at all, so nothing was learned about the target. Raise maxTries, raise timeout_ms, and check the URL resolves from the public internet.

other is high

Answered, and classified by none of the above. Check total_time against your timeout_ms: a target slower than your budget lands here.

Browser Profile Rotation

When a site refuses the browser FourA sent, Proxy stops insisting on it and tries another family from the public profile catalogue. It costs no extra attempt: the rotation changes what a retry sends, never whether one happens.

profilesTried is how you see it happen. One entry means the request went out as written every time. Several mean the rotation ran and the site refused each of them, which is a different situation from never having rotated at all.

On a successful Proxy response, a profile field appears only when the rotation chose a browser you didn't ask for:

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

The value is a catalogue id from GET /api/profiles. Absent means the request went out exactly as written. Present means the browser that worked wasn't the one you typed, so pass that id back as profile on follow-up calls rather than replaying the one that failed. The dashboard Playground does this for you with Carry.

An explicit profile, browser, os, or version on your request is never overridden. Neither is a request carrying your own User-Agent or Cookie header, since a clearance is bound to the signature that earned it.

Reading It in Code

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", [])))
Last updated: August 31, 2026