API Tutorials

Плавная деградация при неудачном решении CAPTCHA

CAPTCHA рано или поздно не решится — не из-за бага в вашем коде, а потому что тайм-ауты, обнулившийся баланс или ограничение частоты запросов встречаются в любом достаточно долго работающем конвейере. Вопрос не «случится ли сбой», а что делает автоматизация в этот момент: падает целиком или продолжает работу в ограниченном режиме.

Плавная деградация — это набор шаблонов, которые превращают единичный отказ в решении CAPTCHA в управляемое событие: пропустить задачу, поставить её в очередь на повтор либо временно снизить нагрузку на API — вместо того чтобы терять весь прогресс пайплайна из-за одной неудачной попытки.


Какие сбои случаются при вызове API CaptchaAI

Прежде чем проектировать восстановление, разделите отказы на три группы — у каждой своя логика реакции:

  • Временные — тайм-аут, ограничение частоты запросов: имеет смысл повторить попытку.
  • Постоянные — плохие параметры, неверный sitekey: повтор не поможет, нужно чинить источник данных.
  • Инфраструктурные — нулевой баланс, обрыв соединения: нужна пауза и оповещение, а не мгновенный повтор.

Полная карта отказов и кодов ошибок:

Отказ Код ошибки Стратегия восстановления
Тайм-аут CAPCHA_NOT_READY (превышено число опросов) Повторить попытку с новым запросом
Плохие параметры ERROR_BAD_PARAMETERS Записать в лог и пропустить — исправить извлечение данных
Неправильный sitekey ERROR_WRONG_GOOGLEKEY Повторно извлечь sitekey со страницы
Нулевой баланс ERROR_ZERO_BALANCE Пауза, оповещение, ожидание пополнения
Превышена частота запросов ERROR_TOO_MUCH_REQUESTS Экспоненциальная задержка (backoff)
API недоступен Ошибка соединения Circuit breaker + повтор

Схема 1: пропустить сбой и продолжить пакетную обработку

Для пакетных операций, где отдельные неудачи не критичны для итогового результата, проще всего пропустить проблемную позицию и двигаться дальше:

Когда применять

  • Для больших пакетов URL, где потеря пары адресов не влияет на общий результат.
  • Когда цена ошибки ниже цены попытки исправить её прямо в моменте.
import requests
import time

API_KEY = "YOUR_API_KEY"


def solve_or_skip(captcha_type, sitekey, page_url, max_retries=2):
    """Try to solve; return None on failure instead of crashing."""
    for attempt in range(max_retries):
        try:
            token = solve_captcha(captcha_type, sitekey, page_url)
            if token:
                return token
        except Exception as e:
            print(f"Attempt {attempt + 1} failed: {e}")

    return None  # Skip this item


def process_urls(urls):
    results = []
    skipped = []

    for url in urls:
        sitekey = extract_sitekey(url)
        if not sitekey:
            skipped.append({"url": url, "reason": "no_sitekey"})
            continue

        token = solve_or_skip("recaptcha_v2", sitekey, url)
        if token:
            data = submit_form(url, token)
            results.append({"url": url, "data": data})
        else:
            skipped.append({"url": url, "reason": "solve_failed"})

    print(f"Processed: {len(results)}, Skipped: {len(skipped)}")
    return results, skipped

Схема 2: очередь повторных попыток

Если задачу нельзя просто выбросить, отправьте её в очередь и обработайте позже — с экспоненциальной задержкой между попытками, чтобы не биться в одну и ту же временную проблему без пауз:

Когда применять

  • Когда у каждой задачи есть ценность и пропускать её нельзя.
  • Когда сбой похож на временный — тайм-аут или ограничение частоты запросов.
from collections import deque
import json

class RetryQueue:
    def __init__(self, max_retries=3, backoff_base=60):
        self.queue = deque()
        self.max_retries = max_retries
        self.backoff_base = backoff_base

    def add(self, task):
        task["retry_count"] = task.get("retry_count", 0) + 1
        if task["retry_count"] <= self.max_retries:
            task["retry_after"] = time.time() + (
                self.backoff_base * task["retry_count"]
            )
            self.queue.append(task)
            return True
        return False  # Exceeded max retries

    def get_ready(self):
        """Get tasks ready for retry."""
        ready = []
        remaining = deque()
        now = time.time()

        while self.queue:
            task = self.queue.popleft()
            if task["retry_after"] <= now:
                ready.append(task)
            else:
                remaining.append(task)

        self.queue = remaining
        return ready

    def save(self, filepath="retry_queue.json"):
        with open(filepath, "w") as f:
            json.dump(list(self.queue), f)

    def load(self, filepath="retry_queue.json"):
        try:
            with open(filepath) as f:
                self.queue = deque(json.load(f))
        except FileNotFoundError:
            pass


# Usage
retry_q = RetryQueue()

def process_with_retry(task):
    try:
        token = solve_captcha(task["type"], task["sitekey"], task["url"])
        if token:
            return submit_form(task["url"], token)
        else:
            retry_q.add(task)
    except Exception:
        retry_q.add(task)

# Process retry queue periodically
def drain_retry_queue():
    ready = retry_q.get_ready()
    for task in ready:
        process_with_retry(task)

Схема 3: деградированный режим при полном отказе API

Когда сервис решения перестаёт отвечать вовсе, точечных повторов уже недостаточно — конвейеру нужен третий уровень защиты: временный ограниченный режим работы.

Когда применять

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

class CaptchaSolver:
    def __init__(self, api_key):
        self.api_key = api_key
        self.degraded = False
        self.failure_count = 0
        self.failure_threshold = 5
        self.recovery_time = None

    def solve(self, captcha_type, sitekey, page_url):
        if self.degraded:
            if time.time() < self.recovery_time:
                return self._degraded_action(page_url)
            else:
                self.degraded = False
                self.failure_count = 0

        try:
            token = self._solve_api(captcha_type, sitekey, page_url)
            self.failure_count = 0
            return token
        except Exception as e:
            self.failure_count += 1
            if self.failure_count >= self.failure_threshold:
                self._enter_degraded_mode()
            raise

    def _enter_degraded_mode(self):
        self.degraded = True
        self.recovery_time = time.time() + 300  # 5 min
        print("Entering degraded mode for 5 minutes")
        # Send alert

    def _degraded_action(self, url):
        """What to do when solving is unavailable."""
        # Option A: Skip CAPTCHA pages entirely
        return None

        # Option B: Queue for later
        # retry_queue.add({"url": url, ...})
        # return None

        # Option C: Try alternative solver
        # return self._solve_with_backup_api(...)

    def _solve_api(self, captcha_type, sitekey, page_url):
        # Normal CaptchaAI API call
        resp = requests.post("https://ocr.captchaai.com/in.php", data={
            "key": self.api_key,
            "method": "userrecaptcha",
            "googlekey": sitekey,
            "pageurl": page_url,
            "json": "1",
        }).json()

        if resp["status"] != 1:
            raise Exception(resp["request"])

        task_id = resp["request"]
        for _ in range(24):
            time.sleep(5)
            result = requests.get("https://ocr.captchaai.com/res.php", params={
                "key": self.api_key, "action": "get",
                "id": task_id, "json": "1"
            }).json()
            if result["status"] == 1:
                return result["request"]
            if result["request"] != "CAPCHA_NOT_READY":
                raise Exception(result["request"])

        raise Exception("TIMEOUT")

Комбинированный сценарий на Node.js

Тот же принцип, собранный в одном классе на Node.js: счётчик отказов, автоматическое восстановление по таймеру и очередь повторов:

class ResilientSolver {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.retryQueue = [];
    this.failureCount = 0;
    this.degraded = false;
  }

  async solve(type, sitekey, pageUrl) {
    if (this.degraded) {
      this.retryQueue.push({ type, sitekey, pageUrl, addedAt: Date.now() });
      return null;
    }

    try {
      const token = await this._callApi(type, sitekey, pageUrl);
      this.failureCount = 0;
      return token;
    } catch (err) {
      this.failureCount++;

      if (err.message === 'ERROR_ZERO_BALANCE') {
        this._enterDegraded(600000); // 10 min
        return null;
      }

      if (this.failureCount >= 5) {
        this._enterDegraded(300000); // 5 min
      }

      this.retryQueue.push({ type, sitekey, pageUrl, addedAt: Date.now() });
      return null;
    }
  }

  _enterDegraded(durationMs) {
    this.degraded = true;
    console.warn(`Degraded mode for ${durationMs / 1000}s`);
    setTimeout(() => {
      this.degraded = false;
      this.failureCount = 0;
      this.drainRetryQueue();
    }, durationMs);
  }

  async drainRetryQueue() {
    const tasks = this.retryQueue.splice(0);
    for (const task of tasks) {
      await this.solve(task.type, task.sitekey, task.pageUrl);
    }
  }

  async _callApi(type, sitekey, pageUrl) {
    // Standard submit + poll
    const axios = require('axios');
    const submit = await axios.post('https://ocr.captchaai.com/in.php', null, {
      params: { key: this.apiKey, method: 'userrecaptcha', googlekey: sitekey, pageurl: pageUrl, json: 1 },
    });
    if (submit.data.status !== 1) throw new Error(submit.data.request);

    const taskId = submit.data.request;
    for (let i = 0; i < 24; i++) {
      await new Promise(r => setTimeout(r, 5000));
      const poll = await axios.get('https://ocr.captchaai.com/res.php', {
        params: { key: this.apiKey, action: 'get', id: taskId, json: 1 },
      });
      if (poll.data.status === 1) return poll.data.request;
      if (poll.data.request !== 'CAPCHA_NOT_READY') throw new Error(poll.data.request);
    }
    throw new Error('TIMEOUT');
  }
}

Что важно в этом примере

  • ERROR_ZERO_BALANCE переводит воркер в более долгий деградированный режим (10 минут), чем прочие сбои (5 минут) — пополнение баланса обычно занимает больше времени, чем сетевой сбой.
  • Очередь повторов дренируется автоматически по таймеру при выходе из деградированного режима — отдельный планировщик не нужен.

Типичные проблемы при внедрении

На практике эти три схемы иногда работают не так, как задумано. Вот частые симптомы и их настоящие причины:

Проблема Причина Исправление
Все задачи пропускаются Деградированный режим срабатывает слишком агрессивно Поднять порог отказов (failure_threshold)
Очередь повторов растёт бесконечно Задачи никогда не решаются успешно Ограничить число повторов; перенести в очередь недоставленных сообщений
Восстановление слишком медленное Слишком длинный тайм-аут деградированного режима Сократить время восстановления; добавить проверку работоспособности API
Очередь теряется при перезапуске Очередь хранится только в памяти процесса Сохранять очередь в файл или базу данных

Совет: прежде чем менять пороги в коде, проверьте, не совпадает ли всплеск отказов по времени с плановым обслуживанием API или сетевыми проблемами на стороне вашего хостинга — иногда «баг» в логике деградации на самом деле внешний сбой.


Матрица решений: повторять, воспроизводить или пропускать

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

Для команд, чьи воркеры крутятся в европейских дата-центрах или разворачиваются ближе к Казахстану и Центральной Азии, нестабильное соединение через мобильные или резидентные прокси — обычное дело: короткий сетевой тайм-аут легко спутать с реальным отказом решения на стороне API. Поэтому порог входа в деградированный режим стоит подбирать с запасом на практике, а не копировать значение по умолчанию из документации. И ещё один практический момент: если вы логируете пропущенные задачи вместе с URL и метаданными формы, храните в логе только то, что действительно нужно для повторной попытки — это упрощает соответствие 152-ФЗ «О персональных данных» для команд, работающих с российской аудиторией, и остаётся хорошей практикой в духе GDPR для остальных.

Часто задаваемые вопросы

Чем плавная деградация отличается от circuit breaker?

Circuit breaker — это бинарный переключатель: он полностью блокирует вызовы API, как только видит слишком много отказов подряд. Плавная деградация шире — это стратегия поведения всего конвейера при сбое: пропустить задачу, поставить её в очередь или переключиться на резервный сценарий. На практике оба шаблона работают вместе: выключатель решает, когда останавливать вызовы, а деградация — что делать с задачами, пока он разомкнут.

Нужно ли повторять попытку при любой ошибке API?

Нет. ERROR_BAD_PARAMETERS и ERROR_WRONG_GOOGLEKEY означают, что запрос собран неправильно — повтор с теми же параметрами провалится точно так же. Повторяйте только временные сбои: тайм-ауты, ERROR_TOO_MUCH_REQUESTS, обрывы соединения. Остальное — записывайте в лог и чините источник данных, обычно это неправильно извлечённый sitekey.

Сколько раз стоит повторять задачу, прежде чем сдаться?

Для большинства пайплайнов достаточно 2–3 попыток с экспоненциальной задержкой — как backoff_base, умноженный на номер попытки, в примере RetryQueue выше. Если после третьей попытки задача всё ещё не решена, дешевле переместить её в очередь недоставленных сообщений и разобраться отдельно, чем бесконечно жечь потоки на заведомо проблемной задаче.

Как не терять очередь повторов при перезапуске сервиса?

In-memory очередь на deque, как в примере выше, исчезает при перезапуске процесса. В продакшене сохраняйте очередь во внешнее хранилище — файл, Redis или таблицу в базе данных — и восстанавливайте её при старте методом load(). Это особенно важно, если деградированный режим включается на несколько минут: за это время в очереди может накопиться заметный объём задач.

Что делать, если баланс потоков закончился в середине рабочего дня?

ERROR_ZERO_BALANCE — не повод для повтора: пока баланс не пополнен, попытки будут проваливаться одинаково. Ставьте конвейер на паузу, отправляйте оповещение ответственному и переходите в деградированный режим до подтверждения пополнения — как показано в примере на Node.js, где именно эта ошибка сразу переводит воркер в degraded на более длинный интервал, чем остальные сбои.


Постройте отказоустойчивую автоматизацию CAPTCHA на CaptchaAI

Получите API-ключ на captchaai.com и добавьте в конвейер очередь повторов и деградированный режим уже сегодня.


Похожие руководства

Комментарии для этой статьи отключены.