Scarnox · Blog

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:

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:

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

Prueba tu gestión de errores con una prueba gratuita: crea una cuenta y empieza con crédito de prueba gratis, sin tarjeta.