Tutorial

Penanganan Error Callback URL CaptchaAI: Retry dan Pola Dead-Letter

Callback (pingback) menghilangkan polling, namun memperkenalkan mode kegagalan baru: apa yang terjadi ketika server Anda down, mengembalikan error, atau timeout saat CaptchaAI mencoba mengirimkan hasilnya? Tutorial ini mencakup pola untuk menangani kegagalan callback tanpa kehilangan hasil solve CAPTCHA.

Apa yang Bisa Salah

Mode Kegagalan Gejala Hasil
Server down CaptchaAI mendapat connection refused Hasil tidak terkirim
Server mengembalikan 5xx CaptchaAI menerima respons error Tidak retry (tergantung implementasi)
Network timeout Koneksi CaptchaAI hang Hasil berpotensi hilang
Handler crash Request diterima tapi hasil tidak tersimpan Hasil diam-diam terbuang

Solusinya: jangan hanya mengandalkan callback. Selalu sediakan fallback.

Pola 1: Callback + Polling Fallback

Pendekatan paling andal — terima callback saat tiba, namun polling untuk task apa pun yang tidak menerima callback dalam batas waktu.

Python

import os
import time
import threading
import requests
from flask import Flask, request

app = Flask(__name__)
API_KEY = os.environ["CAPTCHAAI_API_KEY"]

# Track task state
pending_tasks = {}  # task_id -> {"submitted_at": timestamp, "status": "pending"}
results = {}
lock = threading.Lock()


def submit_captcha(sitekey, pageurl, callback_url):
    """Submit with callback, but track for fallback polling."""
    resp = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY,
        "method": "userrecaptcha",
        "googlekey": sitekey,
        "pageurl": pageurl,
        "pingback": callback_url,
        "json": 1
    })
    data = resp.json()

    if data.get("status") == 1:
        task_id = data["request"]
        with lock:
            pending_tasks[task_id] = {
                "submitted_at": time.time(),
                "status": "pending"
            }
        return task_id
    return None


@app.route("/callback")
def captcha_callback():
    """Primary result delivery — CaptchaAI sends results here."""
    task_id = request.args.get("id")
    solution = request.args.get("code")

    with lock:
        results[task_id] = solution
        pending_tasks.pop(task_id, None)

    return "OK", 200


def fallback_poller():
    """Poll for any tasks that missed their callback."""
    while True:
        time.sleep(30)  # Check every 30 seconds

        with lock:
            stale_tasks = [
                tid for tid, info in pending_tasks.items()
                if time.time() - info["submitted_at"] > 120  # 2 min callback timeout
                and info["status"] == "pending"
            ]

        for task_id in stale_tasks:
            resp = requests.get("https://ocr.captchaai.com/res.php", params={
                "key": API_KEY,
                "action": "get",
                "id": task_id,
                "json": 1
            })
            data = resp.json()

            if data.get("status") == 1:
                with lock:
                    results[task_id] = data["request"]
                    pending_tasks.pop(task_id, None)
                print(f"Fallback poll recovered: {task_id}")
            elif data.get("request") != "CAPCHA_NOT_READY":
                # Permanent error — remove from pending
                with lock:
                    pending_tasks.pop(task_id, None)
                print(f"Task failed: {task_id} — {data.get('request')}")


# Start fallback poller in background
poller_thread = threading.Thread(target=fallback_poller, daemon=True)
poller_thread.start()

JavaScript

const express = require("express");
const axios = require("axios");

const app = express();
const API_KEY = process.env.CAPTCHAAI_API_KEY;

const pendingTasks = new Map(); // taskId -> { submittedAt, status }
const results = new Map();

async function submitCaptcha(sitekey, pageurl, callbackUrl) {
  const resp = await axios.post("https://ocr.captchaai.com/in.php", null, {
    params: {
      key: API_KEY,
      method: "userrecaptcha",
      googlekey: sitekey,
      pageurl: pageurl,
      pingback: callbackUrl,
      json: 1,
    },
  });

  if (resp.data.status === 1) {
    const taskId = resp.data.request;
    pendingTasks.set(taskId, {
      submittedAt: Date.now(),
      status: "pending",
    });
    return taskId;
  }
  return null;
}

// Primary callback endpoint
app.get("/callback", (req, res) => {
  const taskId = req.query.id;
  const solution = req.query.code;

  results.set(taskId, solution);
  pendingTasks.delete(taskId);

  res.sendStatus(200);
});

// Fallback poller
setInterval(async () => {
  const now = Date.now();
  const staleTasks = [];

  for (const [taskId, info] of pendingTasks) {
    if (now - info.submittedAt > 120000 && info.status === "pending") {
      staleTasks.push(taskId);
    }
  }

  for (const taskId of staleTasks) {
    try {
      const resp = await axios.get("https://ocr.captchaai.com/res.php", {
        params: { key: API_KEY, action: "get", id: taskId, json: 1 },
      });

      if (resp.data.status === 1) {
        results.set(taskId, resp.data.request);
        pendingTasks.delete(taskId);
        console.log(`Fallback recovered: ${taskId}`);
      } else if (resp.data.request !== "CAPCHA_NOT_READY") {
        pendingTasks.delete(taskId);
        console.log(`Task failed: ${taskId} — ${resp.data.request}`);
      }
    } catch (err) {
      console.error(`Poll error for ${taskId}: ${err.message}`);
    }
  }
}, 30000);

app.listen(3000);

Pola 2: Dead-Letter Queue

Saat handler callback memproses hasil namun mengalami error (database down, validasi gagal), pindahkan ke dead-letter queue alih-alih kehilangan data.

Python

import json
import os
import time
from pathlib import Path

DEAD_LETTER_DIR = Path("dead_letter")
DEAD_LETTER_DIR.mkdir(exist_ok=True)


@app.route("/callback")
def captcha_callback_with_dlq():
    task_id = request.args.get("id")
    solution = request.args.get("code")

    try:
        # Attempt normal processing
        store_result(task_id, solution)
        return "OK", 200
    except Exception as e:
        # Processing failed — save to dead-letter queue
        dead_letter = {
            "task_id": task_id,
            "solution": solution,
            "error": str(e),
            "received_at": time.time()
        }
        dlq_path = DEAD_LETTER_DIR / f"{task_id}.json"
        dlq_path.write_text(json.dumps(dead_letter))

        print(f"DLQ: {task_id} — {e}")
        return "OK", 200  # Still return 200 to CaptchaAI


def reprocess_dead_letters():
    """Retry processing dead-letter items."""
    for dlq_file in DEAD_LETTER_DIR.glob("*.json"):
        item = json.loads(dlq_file.read_text())

        try:
            store_result(item["task_id"], item["solution"])
            dlq_file.unlink()  # Remove after successful processing
            print(f"DLQ reprocessed: {item['task_id']}")
        except Exception:
            pass  # Leave in DLQ for next retry

JavaScript

const fs = require("fs");
const path = require("path");

const DLQ_DIR = path.join(__dirname, "dead_letter");
if (!fs.existsSync(DLQ_DIR)) fs.mkdirSync(DLQ_DIR);

app.get("/callback-dlq", (req, res) => {
  const taskId = req.query.id;
  const solution = req.query.code;

  try {
    storeResult(taskId, solution);
    res.sendStatus(200);
  } catch (err) {
    // Save to dead-letter queue
    const deadLetter = {
      task_id: taskId,
      solution: solution,
      error: err.message,
      received_at: Date.now(),
    };

    fs.writeFileSync(
      path.join(DLQ_DIR, `${taskId}.json`),
      JSON.stringify(deadLetter)
    );

    console.log(`DLQ: ${taskId} — ${err.message}`);
    res.sendStatus(200); // Still acknowledge to CaptchaAI
  }
});

function reprocessDeadLetters() {
  const files = fs.readdirSync(DLQ_DIR).filter((f) => f.endsWith(".json"));

  for (const file of files) {
    const filePath = path.join(DLQ_DIR, file);
    const item = JSON.parse(fs.readFileSync(filePath, "utf8"));

    try {
      storeResult(item.task_id, item.solution);
      fs.unlinkSync(filePath);
      console.log(`DLQ reprocessed: ${item.task_id}`);
    } catch (err) {
      // Leave in DLQ
    }
  }
}

// Retry DLQ every 5 minutes
setInterval(reprocessDeadLetters, 300000);

Pola 3: Handler Callback Idempoten

Callback mungkin dikirim lebih dari satu kali. Buat handler Anda idempoten:

@app.route("/callback")
def idempotent_callback():
    task_id = request.args.get("id")
    solution = request.args.get("code")

    with lock:
        # Only process if not already handled
        if task_id in results:
            return "OK", 200  # Already processed — skip silently

        results[task_id] = solution
        pending_tasks.pop(task_id, None)

    return "OK", 200

Matriks Keputusan: Pola Mana yang Digunakan

Skenario Pola Terbaik
Volume rendah, downtime sesekali Callback + Polling Fallback
Volume tinggi, kemungkinan gangguan database Dead-Letter Queue
Beberapa consumer mungkin memproses hasil yang sama Handler Idempoten
Sistem produksi dengan SLA Ketiga pola digabungkan

Pemecahan Masalah

Masalah Penyebab Solusi
Polling fallback menemukan task yang sudah dikirim via callback Race condition antara callback dan poller Tambahkan pengecekan idempoten — skip jika hasil sudah ada
DLQ terus tumbuh tanpa diproses Reprocessor tidak berjalan atau gagal Cek log reprocessor; pastikan masalah mendasar (DB) sudah diperbaiki
Callback mengembalikan 200 tapi hasil hilang Handler crash setelah respons dikirim Proses sebelum merespons, atau gunakan pola DLQ
Terlalu banyak request polling fallback Terlalu banyak task basi Tingkatkan threshold timeout callback; cek uptime server

Pertanyaan Umum

Haruskah saya selalu mengembalikan 200 ke callback CaptchaAI?

Ya. Mengembalikan kode error (4xx/5xx) tidak membantu — CaptchaAI mungkin tidak melakukan retry callback. Selalu terima pengiriman (200 OK) dan tangani kegagalan secara internal dengan DLQ atau polling fallback.

Berapa lama menunggu sebelum polling fallback?

Tunggu minimal 120 detik setelah submit. Sebagian besar CAPTCHA diselesaikan dalam 10–60 detik, ditambah latensi jaringan untuk pengiriman callback. Dua menit memberikan waktu yang cukup.

Bisakah saya menonaktifkan callback dan polling saja?

Ya — cukup tidak sertakan parameter pingback. Namun callback mengurangi jumlah panggilan API secara signifikan pada skala besar (2 panggilan per task, bukan 10+ request polling).

Artikel Terkait

  • Validasi Callback Webhook Security CaptchaAI
  • Referensi Kode Error CaptchaAI

Langkah Selanjutnya

Bangun penanganan callback CAPTCHA yang andal — dapatkan API key CaptchaAI Anda dan terapkan pola ketahanan ini.

Panduan terkait:

Komentar dinonaktifkan untuk artikel ini.