Por qué un proxy request se quedó sin intentos
El problema
Una llamada POST /api/proxy/ devuelve un error y ningún dato. El mensaje es corto y siempre tiene la misma forma:
{
"error": "Download maxTry limit reached",
"total": 34.812,
"request": { "...": "..." }
}
Esa frase se lee exactamente igual si cada salida fue bloqueada, si cada salida estaba caída, o si FourA obtuvo la página real en casi todos los intentos y tus propias reglas de validate la descartaron. Esos tres casos requieren soluciones opuestas.
La respuesta: attemptReport
Cada respuesta fallida de Proxy incluye un objeto attemptReport junto al error. Cuenta con qué se encontraron realmente los intentos:
{
"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
}
La cadena error se mantiene deliberadamente sin cambios, de modo que un cliente que coincida con ella siga funcionando. Lee attemptReport.summary para obtener una respuesta de una sola línea, o los conteos cuando quieras utilizarlos para ramificar la lógica.
Campos
| Campo | Tipo | Qué cuenta |
|---|---|---|
total |
integer | Intentos realizados |
noResponse |
integer | La salida nunca respondió, por lo que nunca se llegó al sitio |
defense |
integer | El sitio respondió y se reconoció un proveedor de verificación de bots en esa respuesta |
contentRejected |
integer | HTTP 200, sin verificación de bots, rechazado solo por tu validate.data |
statusRejected |
integer | El sitio respondió, sin verificación de bots, rechazado por tu validate.status |
other |
integer | Respondió, y ninguno de los anteriores |
vendors |
string[] | Cada proveedor de verificación de bots reconocido en cualquier parte de la tarea |
profilesTried |
string[] | Los perfiles de navegador que envió la tarea, en orden de primer uso. default significa que la request se envió exactamente como la escribiste. |
summary |
string | Una oración construida a partir de los conteos. Seguro para registrar en logs o mostrar a un usuario. |
Cómo interpretarlo
contentRejected es alto
Las páginas llegaron. Tu regla validate.data no coincidió con ellas.
Este es el problema que puedes solucionar por tu cuenta, y es el que ocultan todas las demás señales: las requests parecen fallos en cada métrica, pero FourA estuvo entregando contenido real todo el tiempo. Obtén la página una vez a través de POST /api/single/ sin ningún validate, observa qué se recibe realmente y reescribe la regla en base a ello.
Una causa común es aplicar una sola regla a un conjunto de páginas que no son todas iguales. Un selector que existe en las páginas de artículos y no en las páginas de videos falla cada vez que llega a una página de video, de forma permanente y con costo total.
statusRejected es alto
El sitio respondió y tu regla validate.status la rechazó. Si esos estados son 401, 403, 429 o 503, el sitio está rechazando al cliente en lugar de indicar que la página no existe. Prueba:
- Otro perfil de navegador (
browser,os,versionen el objeto internorequest) exitCountriessi el contenido tiene bloqueo regionalPOST /api/browser/si el rechazo requiere JavaScript para resolverse
defense es alto
Se reconoció una verificación de bots en las respuestas, y vendors indica cuál. Consulta Anti-Bot Defenses para ver qué resuelve FourA hoy y qué solo reporta. Si el proveedor no es de los que se resuelven en este endpoint, mueve la llamada a POST /api/browser/ o POST /api/auto/.
noResponse es alto
Las salidas no respondieron en absoluto, por lo que no se obtuvo información sobre el destino. Aumenta maxTries, aumenta timeout_ms y comprueba que la URL se resuelva desde la internet pública.
other es alto
Respondió, y no fue clasificado por ninguno de los casos anteriores. Comprueba total_time frente a tu timeout_ms: un destino más lento que tu presupuesto termina aquí.
Browser Profile Rotation
Cuando un sitio rechaza el navegador que FourA envió, Proxy deja de insistir con él y prueba con otra familia del catálogo público de perfiles. No cuesta ningún intento adicional: la rotación cambia lo que envía un reintento, nunca si este se realiza o no.
profilesTried es la forma en que ves cómo sucede. Una sola entrada significa que la request salió tal como se redactó en cada ocasión. Varias entradas significan que la rotación se ejecutó y el sitio rechazó cada una de ellas, lo cual es una situación diferente a no haber rotado en absoluto.
En una response de Proxy exitosa, aparece un campo profile solo cuando la rotación eligió un navegador que no solicitaste:
{
"status": 200,
"data": "<!doctype html>...",
"proxy": "A1B2C3",
"profile": "...",
"total": 4.108
}
El valor es un id de catálogo de GET /api/profiles. Si está ausente, significa que la request se envió exactamente como se escribió. Si está presente, significa que el navegador que funcionó no fue el que especificaste, así que pasa ese id de vuelta como profile en las llamadas de seguimiento en lugar de repetir el que falló. El Playground del dashboard hace esto por ti con Carry.
Un profile, browser, os o version explícito en tu request nunca se anula. Tampoco se anula una request que lleve tu propio header User-Agent o Cookie, ya que una autorización está vinculada a la firma que la obtuvo.
Leerlo en el 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", [])))
Relacionado
- Endpoints de la API: La referencia completa de request y response de Proxy
- Defensas anti-bot: Proveedores,
defensey cómo reproducir una autorización - Resultados de solicitudes: Cómo se clasifica y factura una respuesta rechazada
- Problemas comunes: Otros problemas y sus soluciones
- Elegir el endpoint adecuado: Cuándo Proxy no es la herramienta correcta