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,versionon the innerrequestobject) exitCountriesif the content is region-lockedPOST /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", [])))
Related
- API Endpoints: The full Proxy request and response reference
- Anti-Bot Defenses: Vendors,
defense, and replaying a clearance - Request Outcomes: How a rejected response is classified and billed
- Common Issues: Other problems and their fixes
- Choosing the Right Endpoint: When Proxy is the wrong tool