Tutorials

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

Server Anda sempat down 30 detik tepat saat CaptchaAI mengirim hasil callback — task-nya sudah selesai diselesaikan di sisi CaptchaAI, tapi hasilnya tidak pernah sampai ke sistem Anda. Ini bukan skenario langka. Callback (pingback) memang menghilangkan kebutuhan polling terus-menerus, tapi ia memperkenalkan mode kegagalan baru yang jarang dipikirkan saat integrasi pertama kali dibangun: server down, respons error, koneksi timeout, sampai handler yang crash setelah request diterima.

Alur dasarnya tetap sama seperti API CaptchaAI pada umumnya, apa pun pola ketahanan yang Anda tambahkan di atasnya:

  1. Kirim task ke in.php
  2. Simpan task_id yang dikembalikan
  3. Terima hasil via callback, atau polling ke res.php sebagai cadangan
  4. Pakai token yang dikembalikan untuk submit form target

Tutorial ini membahas tiga pola produksi untuk langkah 3 — callback dengan fallback polling, dead-letter queue, dan handler idempoten — supaya hasil solve CAPTCHA tidak pernah hilang percuma, sekalipun server Anda sempat bermasalah.

Titik Rawan Kegagalan Callback CaptchaAI

Empat skenario ini paling sering membuat hasil callback CaptchaAI tidak sampai ke server Anda:

  • Server down — CaptchaAI menerima connection refused; hasil solve tidak pernah terkirim.
  • Server balas 5xx — CaptchaAI menerima respons error, dan belum tentu me-retry pengiriman (tergantung implementasi di sisi CaptchaAI).
  • Timeout jaringan — koneksi CaptchaAI menggantung; hasil berisiko hilang.
  • Handler crash — request diterima, tapi hasilnya gagal tersimpan; hasil terbuang tanpa jejak.

Latensi callback yang naik-turun bukan berarti API CaptchaAI bermasalah — itu justru alasan kenapa fallback polling ada.

Solusinya bukan mengejar uptime sempurna — itu ilusi. Solusinya: jangan pernah menggantungkan sistem Anda hanya pada callback. Selalu siapkan jalur cadangan. Konsekuensinya nyata: task yang "hilang" di sisi Anda padahal sudah selesai di sisi CaptchaAI adalah salah satu sumber tiket support paling umum pada integrasi callback — dan biasanya penyebabnya bukan CaptchaAI, tapi tidak adanya jaring pengaman di sisi Anda sendiri.

Pola Mana yang Cocok untuk Skenario Anda

Sebelum masuk ke implementasi, tentukan dulu pola mana yang paling relevan dengan skenario Anda — ketiganya saling melengkapi, bukan saling menggantikan:

  • Volume rendah, downtime sesekali → callback + fallback polling
  • Volume tinggi, database berpotensi down → dead-letter queue
  • Beberapa consumer bisa memproses hasil yang sama → handler idempoten
  • Sistem produksi dengan SLA ketat → gabungkan ketiganya

Bagian berikut membahas ketiga pola tadi secara berurutan, dari yang paling dasar sampai yang paling lengkap.

Pola 1: Callback Plus Fallback Polling

Pendekatan paling tahan banting: terima callback saat datang, tapi polling untuk task apa pun yang belum menerima callback dalam batas waktu tertentu. Ini sangat relevan buat tim automation Indonesia yang menjalankan worker scraping atau price-monitoring di region seperti AWS ap-southeast-3 (Jakarta) atau GCP asia-southeast2 — rute jaringan ke server Anda bisa lebih bervariasi dibanding data center di AS, sehingga latensi pengiriman callback pun ikut naik-turun. Fallback polling jadi jaring pengaman yang murah untuk kondisi seperti ini, bukan tanda ada yang salah dengan API CaptchaAI.

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: Antrean Dead-Letter untuk Hasil yang Gagal Diproses

Kalau handler callback Anda menerima hasil tapi gagal memprosesnya — database sedang down, validasi gagal, atau error lain yang tak terduga — jangan biarkan hasil itu hilang begitu saja. Pindahkan ke dead-letter queue, lalu proses ulang begitu masalah utamanya sudah beres.

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: Bikin Handler Callback Idempoten

CaptchaAI bisa saja mengirim callback yang sama lebih dari satu kali — retry di sisi jaringan mereka, atau load balancer yang menduplikasi request. Pastikan handler Anda aman dipanggil berkali-kali untuk task_id yang sama:

@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

Troubleshooting Callback yang Sering Ditemui

Kenapa fallback polling saya menemukan task yang sebenarnya sudah dikirim via callback?

Ini race condition klasik antara callback dan poller: callback datang duluan, tapi poller sudah keburu jalan sebelum status internal Anda ter-update. Tambahkan pengecekan idempoten di awal handler — kalau task_id sudah ada di results, langsung skip, jangan proses ulang.

Dead-letter queue saya terus bertambah tapi tidak pernah kosong, kenapa?

Biasanya reprocessor-nya tidak berjalan sama sekali, atau berjalan tapi terus gagal karena akar masalahnya (misalnya koneksi database) belum benar-benar diperbaiki. Cek log reprocessor dulu sebelum menambah kapasitas DLQ — DLQ yang terus tumbuh adalah gejala, bukan penyebab.

Callback saya sudah membalas 200 OK, tapi hasilnya tetap hilang. Kenapa bisa begitu?

Tandanya handler Anda crash setelah mengirim respons 200, tapi sebelum hasilnya benar-benar tersimpan. CaptchaAI menganggap pengiriman berhasil begitu menerima 200, jadi tidak akan retry. Proses dan simpan hasilnya dulu sebelum mengirim respons, atau bungkus prosesnya dengan pola dead-letter queue supaya kegagalan penyimpanan tetap tercatat.

Fallback polling saya mengirim terlalu banyak request — kenapa, dan bagaimana menguranginya?

Kemungkinan besar threshold batas waktu callback-nya terlalu ketat, sehingga task yang sebenarnya masih dalam perjalanan sudah dianggap basi dan langsung di-polling. Naikkan threshold-nya (120 detik adalah titik awal yang wajar), lalu cek juga uptime server Anda sendiri — kalau server sering down, jumlah task basi memang akan tinggi apa pun nilai threshold-nya.

Pertanyaan Umum

Apakah saya harus selalu membalas 200 ke callback CaptchaAI?

Ya. Membalas kode error (4xx/5xx) tidak banyak membantu — CaptchaAI belum tentu me-retry pengiriman callback-nya. Selalu terima dulu pengirimannya (200 OK), baru tangani kegagalan pemrosesan secara internal lewat dead-letter queue atau fallback polling.

Berapa lama idealnya menunggu sebelum fallback polling dijalankan?

Minimal 120 detik sejak task dikirim. Sebagian besar CAPTCHA selesai dalam 10–60 detik, ditambah latensi jaringan untuk pengiriman callback-nya. Dua menit sudah memberi ruang yang cukup sebelum Anda menganggap callback-nya gagal dan mulai polling.

Bagaimana cara tahu ada callback yang gagal tanpa menunggu laporan dari user?

Pantau ukuran dead-letter queue dan jumlah task yang jatuh ke fallback polling sebagai metric, lalu pasang alert kalau angkanya melewati ambang batas dalam periode tertentu — misalnya CloudWatch alarm kalau worker Anda jalan di AWS, atau Prometheus dan Alertmanager kalau infrastrukturnya self-hosted. Jangan menunggu tiket support masuk baru sadar ada hasil yang hilang.

Apakah endpoint callback saya perlu dibuka publik, atau bisa di belakang VPN internal?

Endpoint callback harus bisa diakses dari internet publik, karena CaptchaAI mengirim hasilnya dari luar jaringan Anda. Kalau firewall atau security group menutup akses masuk secara default, pastikan path callback-nya dibuka secara spesifik — jangan taruh di belakang autentikasi VPN internal, sebab CaptchaAI tidak bisa login ke VPN Anda untuk mengirimkan hasil.

Bisa pakai Redis atau SQS untuk dead-letter queue, bukan file lokal?

Bisa, dan untuk skala produksi itu justru lebih tepat. Contoh kode di atas memakai file JSON lokal supaya polanya mudah dibaca — prinsipnya tetap sama kalau Anda menggantinya dengan Redis, Amazon SQS, atau tabel database: simpan payload yang gagal diproses, lalu jadwalkan proses ulang sampai berhasil atau melewati batas retry maksimum.

Artikel Terkait

Pelajari juga cara memvalidasi keamanan callback webhook CaptchaAI supaya endpoint Anda tidak menerima payload palsu, dan simpan referensi lengkap kode error CaptchaAI untuk melengkapi penanganan error di luar konteks callback.

Langkah Selanjutnya

Jangan biarkan hasil solve CAPTCHA hilang gara-gara callback yang gagal terkirim — ambil API key CaptchaAI Anda dan terapkan pola-pola ketahanan di atas ke sistem produksi Anda.

Panduan Lanjutan

Komentar dinonaktifkan untuk artikel ini.