Scarnox · Blog

Gestione errori del solver captcha: retry, backoff e cosa non ritentare mai

Una risoluzione captcha è una chiamata di rete verso un sistema che non controlli, per una sfida che può cambiare senza preavviso. Gestire bene i fallimenti è la differenza tra una pipeline che degrada con grazia e una che spreca tempo in una tempesta di retry.

Con Scarnox l'economia ti aiuta: le risoluzioni fallite sono gratuite, quindi un retry costa solo tempo. Ma riprovare l'errore sbagliato spreca comunque quel tempo, e riprovare con troppa insistenza può creare problemi propri. Questa guida ti dà una politica di retry da incollare in un job Python.

Scarnox risolve PopularCaptcha oggi. La stessa gestione degli errori vale per qualsiasi solver captcha, quindi la politica qui sotto non dipende dal tipo di captcha.

Controlla sempre prima errorId

Una risposta di risoluzione ha un campo errorId. 0 significa successo e il token si trova in solution.token. Qualsiasi altro valore significa che la risoluzione è fallita. Una risposta fallita porta anche errorCode e errorDescription. Registra entrambi: il codice è quello che cercherai dopo, e la descrizione è quella che leggerai quando un'esecuzione va storta.

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

Non considerare mai una risposta un successo perché lo stato HTTP è 200. Controlla errorId ogni volta.

Errori da ritentare

Alcuni errori sono temporanei. Riprovali con backoff esponenziale e un piccolo ritardo casuale:

Questi errori meritano un nuovo tentativo perché quello successivo potrebbe riuscire senza alcun cambiamento da parte tua.

Errori da correggere prima di riprovare

Altri errori non spariscono aspettando. Un nuovo tentativo con lo stesso input fallirà allo stesso modo. Correggili prima:

Quando un errore non rientra in nessuna delle due liste, trattalo come un errore da correggere prima. Leggi il testo dell'errore, decidi, e aggiungi il codice alla lista giusta nel tuo codice. Tieni le liste brevi e riviste.

Una politica da incollare

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:
        # Errore di rete o timeout lato nostro: si può ritentare.
        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 ha segnalato un errore. Ritenta solo i codici che hai visto essere temporanei.
    code = data.get("errorCode", "UNKNOWN")
    retryable = code in TRANSIENT_CODES
    raise SolveError(code, data.get("errorDescription", ""), retryable=retryable)

# Aggiungi qui i codici di errore API solo dopo averli visti essere temporanei nei log.
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 casuale
            print(f"attempt {attempt + 1} failed ({err.code}), retrying in {delay:.1f}s")
            time.sleep(delay)
    raise RuntimeError("unreachable")

La politica non riprova mai un errore da correggere, e si ferma dopo un numero fisso di tentativi. L'insieme TRANSIENT_CODES parte vuoto di proposito: aggiungi un codice solo quando i tuoi log mostrano che è temporaneo.

Perché il jitter conta

Se cinquanta worker falliscono nello stesso momento e dormono tutti esattamente due secondi, riproveranno tutti insieme e falliranno di nuovo insieme. Una frazione casuale di secondo li disperde. È lo stesso consiglio che danno la maggior parte dei grandi provider di API ai propri client.

Timeout

Imposta il timeout HTTP ben al di sopra del tempo tipico di risoluzione, circa 185 secondi. La richiesta si blocca finché il token non è pronto, quindi un timeout client corto può trasformare un successo lento in un fallimento che poi riprovi inutilmente. Un timeout client più lungo della risoluzione è l'impostazione sicura.

I token non sono riutilizzabili

Un token scade in circa due minuti. Non memorizzare i token tra un'esecuzione e l'altra, e non riusarne uno per un secondo invio. Se un invio fallisce dopo che il token è stato usato, risolvi di nuovo invece di reinviare il vecchio token. La guida alla scadenza del token approfondisce la tempistica.

Concorrenza e backoff tra worker

Se più worker condividono una chiave, il backoff di un worker non rallenta gli altri. Aggiungi un limite condiviso alle risoluzioni concorrenti, come un semaforo o una coda, così una raffica di retry non diventa una raffica di richieste. Un semplice limite di concorrenza risolve la maggior parte dei problemi di limitazione prima che nascano.

Log utili

Per ogni tentativo fallito, registra il errorCode, il numero del tentativo, la sitekey, l'host della pagina e se l'errore è stato ritentato. Non registrare la chiave API né la password del proxy. Una dashboard che conta i codici per ora ti dice rapidamente se il problema è nel tuo codice, nel proxy o nel servizio.

Prossimi passi

Testa la tua gestione con una prova gratuita: crea un account e inizia con crediti di prova gratuiti, senza carta di credito.