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:
- Panduan Callback URL dan Webhook
- Pola Notifikasi Task Pingback
- Keamanan Webhook: Validasi Callback