Scarnox · Blog

Ошибки решателя капчи: повторы, backoff и что не стоит повторять

Решение капчи — это сетевой вызов к системе, которую вы не контролируете, против проверки, которая может измениться без предупреждения. От того, как вы обрабатываете сбои, зависит, будет ли конвейер деградировать мягко или утонет в шторме повторов.

У Scarnox экономика вам помогает: неудачные решения не списываются, поэтому повтор стоит только времени и больше ничего. Но повтор не того сбоя всё равно тратит время, а слишком настойчивые повторы сами создают проблемы. Этот гайд даёт политику повторов, которую можно вставить в Python-задачу.

Scarnox сегодня решает PopularCaptcha. Та же обработка ошибок применима к любому решателю капчи, поэтому политика ниже не зависит от типа капчи.

Всегда сначала проверяйте errorId

В ответе на решение есть поле errorId. 0 означает успех, и токен лежит в solution.token. Любое другое значение означает, что решение не удалось. В неудачном ответе также есть errorCode и errorDescription. Логируйте оба: код вы будете искать позже, а описание прочитаете, когда что-то пойдёт не так.

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

Никогда не считайте ответ успешным только потому, что HTTP-статус 200. Проверяйте errorId каждый раз.

Сбои, которые стоит повторить

Некоторые сбои временные. Повторяйте их с экспоненциальным backoff и небольшой случайной задержкой:

Эти сбои стоят повтора, потому что следующая попытка может пройти без каких-либо изменений с вашей стороны.

Сбои, которые нужно исправить до повтора

Другие сбои не пройдут сами. Повтор с тем же входом завершится так же. Сначала исправьте их:

Если сбой не попадает ни в один из списков, считайте его сбоем «сначала исправить». Посмотрите текст ошибки, решите и добавьте код в нужный список в своём коде. Держите списки короткими и проверенными.

Политика, которую можно вставить

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:
        # Сетевая ошибка или таймаут на нашей стороне: повтор безопасен.
        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"]

    # API сообщил об ошибке. Повторяем только коды, которые в логах оказались временными.
    code = data.get("errorCode", "UNKNOWN")
    retryable = code in TRANSIENT_CODES
    raise SolveError(code, data.get("errorDescription", ""), retryable=retryable)

# Добавляйте сюда коды ошибок API, только если в логах видно, что они временные.
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 + случайная добавка
            print(f"attempt {attempt + 1} failed ({err.code}), retrying in {delay:.1f}s")
            time.sleep(delay)
    raise RuntimeError("unreachable")

Политика никогда не повторяет сбой «сначала исправить» и останавливается после фиксированного числа попыток. Набор TRANSIENT_CODES намеренно пуст: добавляйте код, только когда логи показали, что он временный.

Зачем нужен jitter

Если пятьдесят воркеров падают в один и тот же момент и все ждут ровно две секунды, они все одновременно повторяют запрос и снова одновременно падают. Случайная доля секунды их разводит. Это тот же совет, который дают большинство крупных API-провайдеров своим клиентам.

Таймауты

Задайте таймаут HTTP заметно выше типичного времени решения, около 185 секунд. Запрос блокируется до готовности токена, поэтому короткий клиентский таймаут может превратить медленный успех в сбой, который вы потом зря повторяете. Таймаут клиента, который длиннее решения, — безопасная настройка.

Токены нельзя кешировать

Токен истекает примерно через две минуты. Не кешируйте токены между запусками и не используйте один токен для второй отправки. Если отправка не удалась после того, как токен уже использовали, решите капчу заново, а не отправляйте старый токен повторно. Подробнее о сроках — в гайде об истечении токена.

Конкурентность и backoff между воркерами

Если много воркеров используют один ключ, backoff в одном воркере не замедляет остальных. Добавьте общий лимит одновременных решений, например семафор или очередь, чтобы всплеск повторов не превратился во всплеск запросов. Простой лимит конкурентности устраняет большинство сбоев из-за ограничения частоты ещё до того, как они начнутся.

Логирование, которое помогает

Для каждой неудачной попытки логируйте errorCode, номер попытки, sitekey, хост страницы и то, был ли сбой повторён. Не логируйте ключ API или пароль прокси. Дашборд, который считает коды по часам, быстро покажет, в вашем коде проблема, в прокси или в сервисе.

Что дальше

Проверьте свою обработку на пробной версии: создайте аккаунт и начните с пробного баланса, карта не нужна.