Scarnox · Blog

Foutafhandeling van de captcha-solver: opnieuw proberen, backoff en wat je nooit opnieuw moet proberen

Een captchaoplossing is een netwerkaanroep naar een systeem dat je niet beheert, tegen een uitdaging die zonder waarschuwing kan veranderen. Goed omgaan met fouten maakt het verschil tussen een pipeline die gracieus achteruitgaat en een pipeline die tijd verspilt in een herhaalstorm.

Bij Scarnox helpt de economie je: mislukte oplossingen zijn gratis, dus een nieuwe poging kost alleen tijd en verder niets. Maar een verkeerde fout opnieuw proberen verspilt die tijd nog steeds, en te hard opnieuw proberen kan eigen problemen veroorzaken. Deze gids geeft je een beleid voor opnieuw proberen dat je in een Python-job kunt plakken.

Scarnox lost vandaag PopularCaptcha op. Dezelfde foutafhandeling geldt voor elke captcha-solver, dus het beleid hieronder hangt niet af van het captchatype.

Controleer altijd eerst errorId

Een oplossingsrespons heeft een veld errorId. 0 betekent succes en het token staat onder solution.token. Elke andere waarde betekent dat de oplossing is mislukt. Een mislukte respons bevat ook een errorCode en een errorDescription. Log beide: de code is wat je later zoekt, en de beschrijving is wat je leest wanneer een run misgaat.

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

Ga nooit uit van succes omdat de HTTP-status 200 is. Controleer errorId elke keer.

Fouten om opnieuw te proberen

Sommige fouten zijn tijdelijk. Probeer deze opnieuw met exponentiële backoff en een kleine willekeurige vertraging:

Deze fouten zijn het opnieuw proberen waard omdat de volgende poging kan slagen zonder dat je iets verandert.

Fouten om eerst te repareren

Andere fouten verdwijnen niet door te wachten. Een nieuwe poging met dezelfde invoer faalt op dezelfde manier. Los deze eerst op:

Wanneer een fout op geen van beide lijsten staat, behandel je hem als een fout die eerst gerepareerd moet worden. Bekijk de foutmelding, beslis, en voeg de code toe aan de juiste lijst in je code. Houd de lijsten kort en laat ze reviewen.

Een beleid om te plakken

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:
        # Netwerkfout of time-out aan onze kant: veilig om opnieuw te proberen.
        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"]

    # De API meldde een fout. Probeer alleen codes opnieuw die je in je logs als tijdelijk hebt gezien.
    code = data.get("errorCode", "UNKNOWN")
    retryable = code in TRANSIENT_CODES
    raise SolveError(code, data.get("errorDescription", ""), retryable=retryable)

# Voeg API-foutcodes hier pas toe nadat je in je logs hebt gezien dat ze tijdelijk zijn.
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")

Het beleid probeert nooit opnieuw een fout die eerst gerepareerd moet worden, en stopt na een vast aantal pogingen. De set TRANSIENT_CODES begint bewust leeg: voeg een code pas toe nadat je logs laten zien dat hij tijdelijk is.

Waarom jitter belangrijk is

Als vijftig workers tegelijk falen en allemaal exact twee seconden slapen, proberen ze gezamenlijk opnieuw en falen ze gezamenlijk opnieuw. Een willekeurig deel van een seconde spreidt ze uit. Dit is hetzelfde advies dat de meeste grote API-aanbieders geven aan hun eigen clients.

Time-outs

Stel je HTTP-time-out ruim in boven de typische oplostijd, ongeveer 185 seconden. Het verzoek blokkeert totdat het token klaar is, dus een korte client-time-out kan een trage maar geslaagde oplossing veranderen in een fout die je vervolgens voor niets opnieuw probeert. Een client-time-out die langer is dan de oplossing is de veilige instelling.

Tokens kunnen niet worden gecachet

Een token verloopt binnen ongeveer twee minuten. Cache tokens niet tussen runs en hergebruik er geen voor een tweede verzending. Als een verzending mislukt nadat het token is gebruikt, los dan opnieuw op in plaats van het oude token opnieuw in te dienen. De gids over tokenverval behandelt de timing in meer detail.

Gelijktijdigheid en backoff over workers heen

Als veel workers één sleutel delen, vertraagt de backoff van één worker de anderen niet. Voeg een gedeelde limiet toe op gelijktijdige oplossingen, zoals een semafoor of een wachtrij, zodat een uitbarsting van nieuwe pogingen niet verandert in een uitbarsting van verzoeken. Een eenvoudige gelijktijdigheidslimiet voorkomt de meeste snelheidslimietfouten voordat ze ontstaan.

Logging die helpt

Log voor elke mislukte poging de errorCode, het pogingnummer, de sitekey, de host van de pagina en of de fout opnieuw is geprobeerd. Log de API-sleutel of een proxywachtwoord niet. Een dashboard dat codes per uur telt, laat snel zien of een probleem in je code, de proxy of de dienst zit.

Volgende stappen

Test je afhandeling tegen een gratis proefversie: maak een account aan en begin met gratis proefcredit, zonder creditcard.