Les rappels (pingbacks) éliminent l'interrogation, mais ils introduisent un nouveau mode d'échec : que se passe-t-il lorsque votre serveur est en panne, renvoie une erreur ou expire lorsque CaptchaAI tente de fournir le résultat ? Ce didacticiel couvre les modèles permettant de gérer les échecs de rappel sans perdre les solutions CAPTCHA.
Qu'est-ce qui peut mal tourner
| Mode de défaillance | Symptôme | Résultat |
|---|---|---|
| Serveur en panne | CaptchaAI obtient une connexion refusée | Solution non livrée |
| Le serveur renvoie 5xx | CaptchaAI reçoit une réponse d'erreur | Impossible de réessayer (dépend de la mise en œuvre) |
| Délai d'expiration du réseau | La connexion CaptchaAI se bloque | Solution potentiellement perdue |
| Le gestionnaire plante | Demande acceptée mais résultat non stocké | Solution abandonnée silencieusement |
La solution : ne comptez jamais uniquement sur les rappels. Ayez toujours une solution de repli.
Modèle 1 : rappel + interrogation de secours
L'approche la plus fiable : acceptez les rappels lorsqu'ils arrivent, mais interrogez toutes les tâches qui ne reçoivent pas de rappels dans un délai d'attente.
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);
Modèle 2 : file d’attente de lettres mortes
Lorsque votre gestionnaire de rappel traite un résultat mais rencontre une erreur (base de données en panne, échec de validation), déplacez le problème vers une file d'attente de lettres mortes au lieu de perdre les données.
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);
Modèle 3 : gestionnaire de rappel idempotent
Les rappels peuvent être envoyés plusieurs fois. Rendre votre gestionnaire idempotent :
@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
Matrice de décision : quel modèle utiliser
| Scénario | Meilleur modèle |
|---|---|
| Faible volume, temps d'arrêt occasionnels | Rappel + interrogation de secours |
| Volume élevé, pannes de base de données possibles | File d'attente de lettres mortes |
| Plusieurs consommateurs peuvent traiter le même résultat | Gestionnaire idempotent |
| Système de production avec SLA | Les trois réunis |
Dépannage
| Problème | Parce que | Corriger |
|---|---|---|
| L'observateur de secours trouve les tâches déjà livrées | Course entre rappel et sondeur | Ajouter un contrôle d'idempotence – ignorer si cela est déjà dans les résultats |
| DLQ grandit sans être traité | Le processeur ne fonctionne pas ou est en panne | Vérifier les journaux du retraiteur ; s'assurer que le problème sous-jacent (DB) est résolu |
| Le rappel renvoie 200 mais le résultat est perdu | Le gestionnaire plante après l'envoi de la réponse | Traiter avant de répondre ou utiliser le modèle DLQ |
| Trop de demandes de sondage de secours | Trop de tâches obsolètes | Augmentez le seuil de délai d'expiration du rappel ; vérifier la disponibilité du serveur |
FAQ
Dois-je toujours renvoyer 200 aux rappels CaptchaAI ?
Oui. Renvoyer un code d'erreur (4xx/5xx) n'aide pas - CaptchaAI ne peut pas réessayer de rappel. Acceptez toujours la livraison (200 OK) et gérez les échecs en interne avec DLQ ou une interrogation de secours.
Combien de temps dois-je attendre avant d’effectuer un sondage de secours ?
Attendez au moins 120 secondes après la soumission. La plupart des CAPTCHA sont résolus en 10 à 60 secondes, plus la latence du réseau pour l'envoi du rappel. Deux minutes donnent suffisamment de temps pour que le rappel arrive.
Puis-je désactiver les rappels et simplement interroger ?
Oui, n'incluez tout simplement pas le paramètre pingback. Mais les rappels réduisent considérablement les appels d'API à grande échelle (2 appels par tâche au lieu de plus de 10 demandes d'interrogation).
Articles connexes
- Python Captcha résolvant les modèles d'erreur de nouvelle tentative
- Validation du rappel de sécurité du Webhook Captchaai
- Référence des codes d'erreur Captchaai
Prochaines étapes
Créez une gestion fiable des rappels CAPTCHA –récupérez votre clé API CaptchaAIet mettre en œuvre ces modèles de résilience.
Guides associés :
- URL de rappel et guide du webhook
- Modèles de notification de tâches Pingback
- Sécurité des webhooks : validation des rappels