Por qué un proxy request se quedó sin intentos

El problema

Una llamada de POST /api/proxy/ devuelve un error y ningún dato. El mensaje es corto y siempre tiene la misma estructura:

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

Esa frase se lee idéntica ya sea que cada salida estuviera bloqueada, cada salida estuviera muerta, o que FourA haya obtenido la página real en casi todos los intentos y tus propias reglas de validate la hayan descartado. Esos tres casos requieren soluciones opuestas.

La respuesta: attemptReport

Cada llamada de Proxy que ejecutó sus intentos y falló incluye un objeto attemptReport junto al error, incluso una que finalizó con "error": "Timeout". Un nombre de host que no se resuelve falla antes de cualquier intento, con status: 0 y sin reporte. 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 en una sola línea, o los conteos cuando quieras bifurcar según ellos.

Campos

Campo Tipo Qué cuenta
total integer Intentos realizados
noResponse integer La salida nunca respondió, o el sitio no respondió a tiempo
defense integer El sitio respondió y se reconoció un proveedor de bot-check en esa respuesta
contentRejected integer HTTP 200, sin bot check, rechazado únicamente por tu validate.data
statusRejected integer El sitio respondió, sin bot check, rechazado por tu validate.status
other integer Respondió, y ninguno de los anteriores
vendors string[] Cada proveedor de bot-check 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 leerlo

contentRejected es alto

Las páginas llegaron. Tu regla validate.data no coincidió con ellas.

Este es el caso que tú mismo puedes solucionar, y es el que cualquier otra señal oculta: las requests parecen fallos en todas las métricas, mientras 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, mira lo que realmente se devuelve 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 de videos falla cada vez que cae en una página de video, indefinidamente y con el 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, version en el objeto interno request)
  • exitCountries si el contenido tiene bloqueo regional
  • POST /api/browser/ si el rechazo requiere JavaScript para resolverse

defense es alto

Se reconoció un bot check en las respuestas, y vendors indica cuál. Consulta Site checks para ver qué resuelve FourA actualmente y qué solo reporta. Si el proveedor no es uno 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 verifica que la URL resuelva desde la internet pública.

other es alto

El sitio respondió con un estado, no se reconoció ningún bot check y el intento falló por otra razón, como una regla validate.headers. Un destino más lento que tu timeout_ms cuenta bajo noResponse.

Rotación de perfiles de navegador

Cuando un sitio rechaza el navegador enviado por FourA, Proxy deja de insistir con él y prueba otra familia del catálogo público de perfiles. Esto no cuesta ningún intento adicional: la rotación cambia lo que envía un reintento, nunca si este se realiza o no.

Proxy también recuerda, por sitio, la última familia que funcionó allí después de que el sitio rechazara la predeterminada. Una request posterior a ese sitio puede comenzar con esa familia desde su primer intento, y FourA pasa a la siguiente familia una vez que el sitio comienza a rechazar esa también. Solo se recuerda un navegador móvil para un sitio que responde JSON.

profilesTried es la forma en que ves cómo sucede. Una sola entrada significa que cada intento envió el mismo navegador: default es la request tal como fue escrita, y un ID de catálogo es la familia que FourA ya había aprendido para ese sitio. 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 exitosa de Proxy, 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 indicaste, así que pasa ese ID como profile en las siguientes llamadas 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 incluya 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", [])))
Actualizado: 30 de septiembre de 2026