Gestión de errores del solver de captchas: reintentos, backoff y qué nunca reintentar
Una resolución de captcha es una llamada de red a un sistema que no controlas, contra un desafío que puede cambiar sin aviso. Gestionar bien los fallos es la diferencia entre un pipeline que se degrada con elegancia y uno que pierde tiempo en una tormenta de reintentos.
Con Scarnox la economía te ayuda: las resoluciones fallidas son gratuitas, así que un reintento cuesta tiempo y nada más. Pero reintentar el fallo equivocado sigue desperdiciando ese tiempo, y reintentar con demasiada insistencia puede causar problemas propios. Esta guía te da una política de reintentos que puedes pegar en un job de Python.
Scarnox resuelve PopularCaptcha hoy. La misma gestión de errores se aplica a cualquier solver de captchas, así que la política de abajo no depende del tipo de captcha.
Comprueba siempre errorId primero
La respuesta de una resolución tiene un campo errorId. 0 significa éxito y el token está en solution.token. Cualquier otro valor significa que la resolución falló. Una respuesta fallida también trae un errorCode y un errorDescription. Regístralos ambos: el código es lo que buscarás después, y la descripción es lo que leerás cuando una ejecución vaya mal.
{"errorId": 1, "errorCode": "...", "errorDescription": "..."}
Nunca des por hecho que una respuesta es un éxito porque el estado HTTP sea 200. Comprueba errorId cada vez.
Fallos que conviene reintentar
Algunos fallos son temporales. Reintenta estos con backoff exponencial y un pequeño retardo aleatorio:
- Errores de red: reinicios de conexión, fallos de DNS, timeouts de lectura en tu lado.
- Errores del servidor: respuestas HTTP 5xx, que indican un problema en el servicio.
- Límite de frecuencia: HTTP 429, que significa que envías peticiones demasiado rápido. Ve más despacio.
- Timeouts: la resolución tardó más de lo que esperó tu cliente. Reintenta con un timeout más largo si aún no lo has configurado.
Estos fallos merecen un reintento porque el siguiente intento puede tener éxito sin que tú cambies nada.
Fallos que hay que corregir antes de reintentar
Otros fallos no desaparecen esperando. Un reintento con la misma entrada fallará igual. Corrige estos primero:
- Tipo de tarea no válido. Comprueba que
captcha_typeesté escrito exactamente como en la documentación. - Sitekey incorrecta o ausente. Vuelve a leer la sitekey de la página. Una sitekey de un despliegue anterior es una causa habitual.
- URL de página incorrecta.
site_urldebe ser la página que muestra el widget. - Problemas de proxy. Un proxy caído, mal escrito o bloqueado hará fallar cada intento. Prueba primero el proxy por separado. Consulta proxy frente a sin proxy.
- Problemas de cuenta. Una clave no válida o una cuenta sin el saldo necesario fallará en cada intento hasta que alguien arregle la cuenta. Avisa a una persona; no reintentes en bucle.
Cuando un fallo no está en ninguna de las dos listas, trátalo como un fallo que hay que corregir primero. Mira el texto del error, decide y añade el código a la lista correcta en tu código. Mantén las listas cortas y revisadas.
Una política que puedes pegar
import os
import random
import time
import requests
API_URL = "https://api.scarnox.com/api/tasks/create"
HEADERS = {"Authorization": f"Bearer {os.environ['SCARNOX_KEY']}"}
MAX_ATTEMPTS = 4
class SolveError(Exception):
def __init__(self, code: str, description: str, retryable: bool):
super().__init__(f"{code}: {description}")
self.code = code
self.retryable = retryable
def solve_once(payload: dict) -> str:
try:
r = requests.post(API_URL, headers=HEADERS, json=payload, timeout=185)
except requests.RequestException as exc:
# Network error or timeout on our side: safe to retry.
raise SolveError("NETWORK", str(exc), retryable=True)
if r.status_code == 429 or r.status_code >= 500:
raise SolveError(f"HTTP_{r.status_code}", r.text[:200], retryable=True)
try:
data = r.json()
except ValueError:
raise SolveError(f"HTTP_{r.status_code}", r.text[:200], retryable=False)
if data.get("errorId") == 0:
return data["solution"]["token"]
# The API reported a failure. Retry only codes you have seen to be transient.
code = data.get("errorCode", "UNKNOWN")
retryable = code in TRANSIENT_CODES
raise SolveError(code, data.get("errorDescription", ""), retryable=retryable)
# Add API error codes here only after you have seen them to be transient in your logs.
TRANSIENT_CODES: set[str] = set()
def solve(payload: dict) -> str:
for attempt in range(MAX_ATTEMPTS):
try:
return solve_once(payload)
except SolveError as err:
last_attempt = attempt == MAX_ATTEMPTS - 1
if not err.retryable or last_attempt:
raise
delay = min(30, 2 ** attempt) + random.random() # backoff + jitter
print(f"attempt {attempt + 1} failed ({err.code}), retrying in {delay:.1f}s")
time.sleep(delay)
raise RuntimeError("unreachable")
La política nunca reintenta un fallo que hay que corregir primero, y se detiene tras un número fijo de intentos. El conjunto TRANSIENT_CODES empieza vacío a propósito: añade un código solo cuando tus registros demuestren que es temporal.
Por qué importa el jitter
Si cincuenta workers fallan en el mismo momento y todos duermen exactamente dos segundos, reintentan juntos y vuelven a fallar juntos. Una fracción aleatoria de segundo los dispersa. Es el mismo consejo que dan la mayoría de los grandes proveedores de API a sus propios clientes.
Timeouts
Configura el timeout HTTP muy por encima del tiempo típico de resolución, unos 185 segundos. La petición se bloquea hasta que el token está listo, así que un timeout corto puede convertir un éxito lento en un fallo que luego reintentas para nada. Un timeout de cliente más largo que la resolución es la configuración segura.
Los tokens no se pueden cachear
Un token caduca en unos dos minutos. No guardes tokens entre ejecuciones ni reutilices uno para un segundo envío. Si un envío falla después de usar el token, resuelve de nuevo en lugar de reenviar el token antiguo. La guía de caducidad del token cubre el timing con más detalle.
Concurrencia y backoff entre workers
Si muchos workers comparten una clave, el backoff de un worker no frena a los demás. Añade un límite compartido de resoluciones concurrentes, como un semáforo o una cola, para que una ráfaga de reintentos no se convierta en una ráfaga de peticiones. Un límite simple de concurrencia evita la mayoría de los fallos por límite de frecuencia antes de que empiecen.
Registros que ayudan
Para cada intento fallido, registra el errorCode, el número de intento, la sitekey, el host de la página y si el fallo se reintentó. No registres la clave de API ni la contraseña de un proxy. Un panel que cuenta códigos por hora te indica rápido si el problema está en tu código, en el proxy o en el servicio.
Próximos pasos
- Cómo resolver un PopularCaptcha en Python
- Proxy o sin proxy: ¿qué tipo de tarea necesitas?
- Mantener CI en verde cuando tu app tiene un captcha
- Prueba gratuita del solver de captchas
Prueba tu gestión de errores con una prueba gratuita: crea una cuenta y empieza con crédito de prueba gratis, sin tarjeta.