Scarnox · Blog

Gestion des erreurs du solveur de captcha : réessais, backoff et ce qu'il ne faut jamais relancer

Une résolution de captcha est un appel réseau vers un système que vous ne contrôlez pas, pour un défi qui peut changer sans prévenir. Bien gérer les échecs fait la différence entre un pipeline qui se dégrade proprement et un pipeline qui perd son temps dans une tempête de réessais.

Avec Scarnox, l'économie joue en votre faveur : les résolutions échouées sont gratuites, un réessai ne coûte donc que du temps. Mais relancer la mauvaise erreur gaspille quand même ce temps, et relancer trop fort peut poser ses propres problèmes. Ce guide vous donne une politique de réessai à coller dans un job Python.

Scarnox résout PopularCaptcha aujourd'hui. La même gestion des erreurs s'applique à n'importe quel solveur de captcha, la politique ci-dessous ne dépend donc pas du type de captcha.

Vérifiez toujours d'abord errorId

Une réponse de résolution contient un champ errorId. 0 signifie succès, et le token se trouve dans solution.token. Toute autre valeur signifie que la résolution a échoué. Une réponse en échec contient aussi un errorCode et un errorDescription. Journalisez les deux : le code est ce que vous chercherez plus tard, et la description est ce que vous lirez lorsqu'une exécution tournera mal.

{"errorId": 1, "errorCode": "...", "errorDescription": "..."}

Ne supposez jamais qu'une réponse est un succès parce que le statut HTTP est 200. Vérifiez errorId à chaque fois.

Les échecs à relancer

Certains échecs sont temporaires. Relancez-les avec un backoff exponentiel et un petit délai aléatoire :

Ces échecs méritent un réessai, car la tentative suivante peut réussir sans aucun changement de votre part.

Les échecs à corriger avant de relancer

D'autres échecs ne disparaîtront pas en attendant. Un réessai avec la même entrée échouera de la même manière. Corrigez-les d'abord :

Lorsqu'un échec ne figure dans aucune de ces listes, traitez-le comme un échec à corriger d'abord. Regardez le texte de l'erreur, décidez, et ajoutez le code à la bonne liste dans votre code. Gardez les listes courtes et relues.

Une politique à copier-coller

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:
        # Erreur réseau ou délai dépassé de notre côté : on peut relancer sans risque.
        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"]

    # L'API a signalé un échec. Ne relancez que les codes que vous avez vus comme transitoires.
    code = data.get("errorCode", "UNKNOWN")
    retryable = code in TRANSIENT_CODES
    raise SolveError(code, data.get("errorDescription", ""), retryable=retryable)

# N'ajoutez des codes d'erreur de l'API ici qu'après les avoir vus comme transitoires dans vos journaux.
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 + gigue
            print(f"attempt {attempt + 1} failed ({err.code}), retrying in {delay:.1f}s")
            time.sleep(delay)
    raise RuntimeError("unreachable")

La politique ne relance jamais un échec à corriger d'abord, et elle s'arrête après un nombre fixe de tentatives. L'ensemble TRANSIENT_CODES commence volontairement vide : n'ajoutez un code qu'une fois que vos journaux montrent qu'il est temporaire.

Pourquoi la gigue compte

Si cinquante workers échouent au même moment et dorment tous exactement deux secondes, ils relancent tous ensemble et échouent de nouveau ensemble. Une fraction de seconde aléatoire les répartit. C'est le même conseil que donnent la plupart des grands fournisseurs d'API à leurs propres clients.

Délais d'attente

Réglez votre délai HTTP nettement au-dessus du temps de résolution typique, environ 185 secondes. La requête bloque jusqu'à ce que le token soit prêt, donc un délai client trop court peut transformer un succès lent en échec que vous relancez pour rien. Un délai client plus long que la résolution est le réglage sûr.

Les tokens ne se mettent pas en cache

Un token expire en environ deux minutes. Ne les mettez pas en cache entre deux exécutions, et ne réutilisez pas un token pour une seconde soumission. Si une soumission échoue après que le token a été utilisé, résolvez à nouveau au lieu de renvoyer l'ancien token. Le guide sur l'expiration des tokens détaille le timing.

Concurrence et backoff entre workers

Si plusieurs workers partagent une même clé, le backoff d'un worker ne ralentit pas les autres. Ajoutez une limite partagée sur les résolutions simultanées, comme un sémaphore ou une file d'attente, pour qu'une rafale de réessais ne se transforme pas en rafale de requêtes. Un simple plafond de concurrence résout la plupart des échecs de limitation de débit avant même qu'ils ne surviennent.

Des journaux utiles

Pour chaque tentative échouée, journalisez le errorCode, le numéro de tentative, la sitekey, l'hôte de la page et si l'échec a été relancé. Ne journalisez jamais la clé d'API ni le mot de passe d'un proxy. Un tableau de bord qui compte les codes par heure vous indique rapidement si le problème vient de votre code, du proxy ou du service.

Étapes suivantes

Testez votre gestion des erreurs avec un essai gratuit : créez un compte et démarrez avec un crédit d'essai gratuit, sans carte bancaire.