Ошибки решателя капчи: повторы, backoff и что не стоит повторять
Решение капчи — это сетевой вызов к системе, которую вы не контролируете, против проверки, которая может измениться без предупреждения. От того, как вы обрабатываете сбои, зависит, будет ли конвейер деградировать мягко или утонет в шторме повторов.
У Scarnox экономика вам помогает: неудачные решения не списываются, поэтому повтор стоит только времени и больше ничего. Но повтор не того сбоя всё равно тратит время, а слишком настойчивые повторы сами создают проблемы. Этот гайд даёт политику повторов, которую можно вставить в Python-задачу.
Scarnox сегодня решает PopularCaptcha. Та же обработка ошибок применима к любому решателю капчи, поэтому политика ниже не зависит от типа капчи.
Всегда сначала проверяйте errorId
В ответе на решение есть поле errorId. 0 означает успех, и токен лежит в solution.token. Любое другое значение означает, что решение не удалось. В неудачном ответе также есть errorCode и errorDescription. Логируйте оба: код вы будете искать позже, а описание прочитаете, когда что-то пойдёт не так.
{"errorId": 1, "errorCode": "...", "errorDescription": "..."}
Никогда не считайте ответ успешным только потому, что HTTP-статус 200. Проверяйте errorId каждый раз.
Сбои, которые стоит повторить
Некоторые сбои временные. Повторяйте их с экспоненциальным backoff и небольшой случайной задержкой:
- Сетевые ошибки: сбросы соединения, сбои DNS, таймауты чтения на вашей стороне.
- Ошибки сервера: ответы HTTP 5xx, которые означают проблему на стороне сервиса.
- Ограничение частоты: HTTP 429, то есть вы отправляете запросы слишком быстро. Замедлитесь.
- Таймауты: решение заняло больше времени, чем ждал ваш клиент. Повторите с более длинным таймаутом, если вы его ещё не задали.
Эти сбои стоят повтора, потому что следующая попытка может пройти без каких-либо изменений с вашей стороны.
Сбои, которые нужно исправить до повтора
Другие сбои не пройдут сами. Повтор с тем же входом завершится так же. Сначала исправьте их:
- Неверный тип задачи. Проверьте, что
captcha_typeнаписан точно так же, как в документации. - Неверный или отсутствующий sitekey. Считайте sitekey со страницы заново. Sitekey из старого деплоя — частая причина.
- Неверный URL страницы.
site_urlдолжен быть страницей, на которой показан виджет. - Проблемы с прокси. Мёртвый, опечатанный или заблокированный прокси провалит каждую попытку. Сначала проверьте прокси отдельно. См. прокси или proxyless.
- Проблемы с аккаунтом. Неверный ключ или аккаунт без нужного баланса будут проваливать каждую попытку, пока кто-то не исправит аккаунт. Оповестите человека; не повторяйте в цикле.
Если сбой не попадает ни в один из списков, считайте его сбоем «сначала исправить». Посмотрите текст ошибки, решите и добавьте код в нужный список в своём коде. Держите списки короткими и проверенными.
Политика, которую можно вставить
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 или пароль прокси. Дашборд, который считает коды по часам, быстро покажет, в вашем коде проблема, в прокси или в сервисе.
Что дальше
- Как решить PopularCaptcha на Python
- Прокси или proxyless: какой тип задачи нужен?
- Капча в e2e-тестах: как держать CI зелёным
- Бесплатная пробная версия решателя капчи
Проверьте свою обработку на пробной версии: создайте аккаунт и начните с пробного баланса, карта не нужна.