回调 (pingback) 消除了轮询,但引入了一种新的故障模式:当您的服务器关闭、返回错误或 CaptchaAI 尝试传递结果时超时时会发生什么?本教程介绍了在不丢失验证码解决方案的情况下处理回调失败的模式。
可能会出现什么问题
| 失效模式 | 症状 | 结果 |
|---|---|---|
| 服务器宕机 | CaptchaAI 连接被拒绝 | 解决方案未交付 |
| 服务器返回5xx | CaptchaAI 收到错误响应 | 可能无法重试(取决于实施) |
| 网络超时 | CaptchaAI 连接挂起 | 解决方案可能会丢失 |
| 处理程序崩溃 | 请求已接受,但结果未存储 | 解决方案默默下降 |
解决方案:永远不要仅仅依赖回调。总是有后备力量。
模式 1:回调 + 回退轮询
最可靠的方法 - 当回调到达时接受回调,但轮询任何在超时内未收到回调的任务。
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);
模式 2:死信队列
当回调处理程序处理结果但遇到错误(数据库关闭、验证失败)时,请将问题移至死信队列而不是丢失数据。
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);
模式 3:幂等回调处理程序
回调可能会多次传递。使您的处理程序具有幂等性:
@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
决策矩阵:使用哪种模式
| 设想 | 最佳模式 |
|---|---|
| 产量低,偶尔出现停机 | 回调+回退轮询 |
| 数据量大,可能会出现数据库中断 | 死信队列 |
| 多个消费者可能会处理相同的结果 | 幂等处理程序 |
| 具有 SLA 的生产系统 | 三者结合起来 |
故障排除
| 问题 | 原因 | 处理方式 |
|---|---|---|
| 后备轮询器查找已交付的任务 | 回调和轮询器之间的竞争 | 添加幂等性检查 - 如果已在结果中则跳过 |
| DLQ 未经处理而增长 | 洗消机未运行或出现故障 | 检查洗消机日志;确保根本问题 (DB) 得到解决 |
| 回调返回200但结果丢失 | 发送响应后处理程序崩溃 | 响应前处理,或使用 DLQ 模式 |
| 后备轮询请求过多 | 太多陈旧的任务 | 增加回调超时阈值;检查服务器正常运行时间 |
常问问题
我应该始终向 CaptchaAI 回调返回 200 吗?
是的。返回错误代码 (4xx/5xx) 没有帮助 – CaptchaAI 可能不会重试回调。始终接受交付 (200 OK) 并使用 DLQ 或后备轮询在内部处理故障。
在后备轮询之前我应该等待多长时间?
提交后至少等待 120 秒。大多数验证码会在 10-60 秒内解决,再加上回调传递的网络延迟。两分钟为回调到达提供了充足的时间。
我可以禁用回调并只进行轮询吗?
是的 - 只是不包含 pingback 参数。但是回调会大规模地减少 API 调用(每个任务 2 次调用,而不是 10 多个轮询请求)。
相关文章
- Python Captcha 解决重试错误模式
- Captchaai Webhook 安全回调验证
- 验证码错误代码参考
下一步
构建可靠的验证码回调处理 -”获取您的 CaptchaAI API 密钥并实施这些弹性模式。
相关指南:
- 回调 URL 和 Webhook 指南
- Pingback 任务通知模式
- Webhook 安全性:验证回调