Hướng Dẫn API

Xuống cấp nhẹ nhàng khi giải CAPTCHA thất bại: giữ automation chạy

Một request giải CAPTCHA lỗi không nên làm cả batch dừng — bài học dev automation thường chỉ rút ra sau khi pipeline crash lúc 2 giờ sáng.

Một agency QA ở TP.HCM chạy batch 5.000 URL checkout mỗi đêm qua CaptchaAI trên gói STANDARD ($30/tháng, 15 thread): không xử lý lỗi mềm, một ERROR_TOO_MUCH_REQUESTS ở URL 42 làm cả script dừng, mất 4.958 URL còn lại. Xuống cấp nhẹ nhàng: bỏ qua, xếp hàng thử lại, hoặc chuyển sang chế độ giới hạn.


Vì sao request giải CAPTCHA thất bại

Cần biết lỗi nào đáng thử lại:

Thất bại Mã lỗi Chiến lược phục hồi
Hết thời gian chờ CAPCHA_NOT_READY Thử lại với thử thách mới
Tham số sai ERROR_BAD_PARAMETERS Ghi log và bỏ qua
Sai sitekey ERROR_WRONG_GOOGLEKEY Trích xuất lại sitekey
Hết số dư ERROR_ZERO_BALANCE Tạm dừng, cảnh báo, chờ nạp tiền
Bị giới hạn tần suất ERROR_TOO_MUCH_REQUESTS Backoff theo cấp số nhân
API không phản hồi Lỗi kết nối Circuit breaker + thử lại

Chỉ 3/6 lỗi trên đáng retry ngay; 3 lỗi còn lại cần sửa dữ liệu hoặc nạp tiền.

Nên retry

  • Timeout, rate limit — lỗi tạm thời.
  • Tham số dựng lại được xác định.

Nên bỏ qua

  • ERROR_BAD_PARAMETERS, ERROR_WRONG_GOOGLEKEY — lỗi ở dữ liệu gửi đi.
  • Lỗi mà thử thêm chỉ lãng phí số dư.

Pattern 1: Bỏ qua và chạy tiếp

Phù hợp cho xử lý theo lô, nơi vài item lỗi không ảnh hưởng kết quả.

  • Dùng khi: crawler giá Shopee, Tiki — bỏ sản phẩm lỗi, giữ phiên chạy.
  • Tránh khi: mỗi item bắt buộc xử lý.
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

Pattern 2: Retry queue (hàng đợi thử lại)

Khi bỏ qua là chưa đủ và mọi task cần được xử lý.

  • Dùng khi: mỗi task có giá trị riêng (đơn hàng, form đăng ký).
  • Cơ chế: task lỗi vào hàng đợi, backoff tăng theo số lần retry.
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)

Pattern 3: Chế độ xuống cấp (degraded mode)

Khi số lỗi liên tiếp vượt ngưỡng, đừng gửi request như bình thường.

  • Dùng khi: ERROR_ZERO_BALANCE hoặc API không phản hồi nhiều lần.
  • Cơ chế: chuyển solver sang chế độ giới hạn trong thời gian cố định.

Lưu ý: failure_threshold quá thấp khiến batch bình thường cũng bị coi là lỗi.

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")

Pattern nào dùng khi nào

Pattern Dùng khi Không dùng khi
Bỏ qua Batch lớn, item lỗi chấp nhận được Mỗi item bắt buộc xử lý
Retry queue Task nào cũng phải hoàn tất Cần phản hồi tức thì
Degraded mode Lỗi hệ thống lặp lại Lỗi ở một task đơn lẻ

Node.js: kết hợp cả ba pattern

Ví dụ dưới đây gộp retry queue và degraded mode trong một class.

  • Chế độ ngắn (5 phút) cho lỗi liên tiếp.
  • Chế độ dài (10 phút) riêng cho ERROR_ZERO_BALANCE.
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');
  }
}

Xử lý sự cố thường gặp

Vấn đề Nguyên nhân Cách xử lý
Tất cả task bị bỏ qua Ngưỡng xuống cấp quá thấp Tăng failure_threshold
Retry queue phình to Task không bao giờ thành công Giới hạn retry; đẩy sang dead letter queue
Phục hồi quá chậm recovery_time quá dài Rút ngắn; thêm health-check probe
Mất task khi restart Queue chỉ nằm trong bộ nhớ Ghi queue xuống file hoặc database

Mẹo: log lý do skip mỗi item để review batch sau.


Câu hỏi thường gặp

Graceful degradation khác circuit breaker ở điểm nào?

Circuit breaker chặn hẳn cuộc gọi khi phát hiện lỗi liên tiếp. Graceful degradation rộng hơn, gồm cả fallback và logic bỏ qua.

Có nên retry mọi task giải CAPTCHA thất bại không?

Không. ERROR_BAD_PARAMETERSERROR_WRONG_GOOGLEKEY sẽ thất bại y hệt. Chỉ retry lỗi tạm thời như CAPCHA_NOT_READY.

Ngưỡng thất bại bao nhiêu thì nên vào chế độ xuống cấp?

Không có con số cố định. Bài này dùng failure_threshold = 5 cho batch vài nghìn URL/đêm; job real-time nên dùng ngưỡng thấp hơn (2–3).

Retry queue nên lưu ở đâu để không mất dữ liệu khi restart?

File JSON đủ cho script một máy; nhiều worker nên dùng Redis hoặc database.


Xây dựng automation CAPTCHA ổn định với CaptchaAI

Nhận API key tại captchaai.com và áp dụng pattern trên cho batch tiếp theo.


Hướng dẫn liên quan

Os comentários estão desativados para este artigo.