Un callback perdu, c'est une solution CAPTCHA déjà payée que votre application ne verra jamais arriver. Les pingbacks CaptchaAI suppriment le polling, mais ils déplacent la fragilité vers votre endpoint : s'il tombe, renvoie une erreur ou expire à la livraison, le résultat est perdu. Voici trois modèles pour qu'aucune résolution ne disparaisse, même quand votre serveur flanche.
Où un callback peut échouer
Une livraison peut casser de quatre façons :
- Serveur hors service — « connection refused » côté CaptchaAI ; solution non livrée.
- Réponse 5xx — le serveur répond en erreur ; CaptchaAI ne relancera pas forcément la livraison.
- Timeout réseau — connexion suspendue, solution potentiellement perdue.
- Gestionnaire qui plante — requête acceptée mais résultat non enregistré : solution perdue en silence.
Ne dépendez jamais uniquement des callbacks : chaque tâche doit avoir un filet de sécurité qui la rattrape.
Modèle 1 : callback avec polling de secours
L'approche la plus robuste combine les deux : acceptez les callbacks quand ils arrivent, et interrogez (polling) toute tâche restée muette trop longtemps. Un worker sur Scaleway qui redémarre en plein déploiement renvoie une connexion refusée sur /callback et perd les callbacks de cette fenêtre ; le poller de secours les rattrape. Implémentation 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()
La version Node.js suit la même logique : un endpoint /callback pour la livraison, un setInterval pour interroger les tâches obsolètes.
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
Ici le callback arrive, mais son traitement échoue : base de données injoignable, validation qui casse, exception inattendue. Plutôt que de perdre la donnée, écrivez-la dans une file de lettres mortes (dead-letter queue) et rejouez-la une fois la panne résolue. Le réflexe clé : accusez quand même réception avec un 200.
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
En Node.js, le principe est identique : un fichier JSON par échec, qu'un rejeu périodique retente puis supprime en cas de succès.
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 : un gestionnaire de callback idempotent
Un même callback peut être livré plusieurs fois. Rendez le gestionnaire idempotent : s'il connaît déjà la tâche, il répond 200 sans rien refaire, sans écraser le résultat ni rejouer d'effet de bord.
@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
Trois réflexes suffisent :
- Vérifiez la présence de la tâche avant tout traitement.
- Répondez 200 si elle est déjà connue, sans rejouer les effets de bord.
- Protégez l'état partagé par un verrou (
lock) contre les situations de course.
Quel modèle choisir selon votre charge
Ils se cumulent selon votre volume et vos exigences :
- Faible volume, coupures ponctuelles → callback avec polling de secours.
- Volume élevé, pannes de base possibles → file de lettres mortes.
- Plusieurs consommateurs pour un même résultat → gestionnaire idempotent.
- Production sous engagement de SLA → les trois combinés.
Dépannage
| Problème | Cause probable | Correctif |
|---|---|---|
| Le poller de secours retrouve des tâches déjà livrées | Course entre le callback et le poller | Ajoutez un contrôle d'idempotence : ignorez la tâche déjà présente dans les résultats |
| La file de lettres mortes grossit sans être vidée | Le rejeu ne tourne pas ou échoue | Vérifiez les logs du rejeu et corrigez la panne sous-jacente (base de données) |
| Le callback répond 200 mais le résultat disparaît | Le gestionnaire plante après la réponse | Traitez avant de répondre, ou passez au modèle de file de lettres mortes |
| Trop de requêtes de polling de secours | Trop de tâches obsolètes | Augmentez le seuil de délai avant bascule et vérifiez la disponibilité de l'endpoint |
FAQ
Comment détecter qu'un callback n'est jamais arrivé ?
Suivez chaque tâche dans une table pending_tasks horodatée. Toute entrée qui dépasse votre délai (120 secondes) sans callback devient candidate au polling de secours : c'est le rôle du poller du Modèle 1.
Faut-il vraiment répondre 200 même en cas d'échec interne ?
Oui. Une 4xx ou une 5xx n'aide pas, car CaptchaAI ne rejoue pas forcément le callback. Accusez réception avec un 200, puis gérez l'échec en interne.
La file de lettres mortes doit-elle vivre en base de données ?
Pas nécessairement. De simples fichiers JSON sur disque suffisent à faible volume. À grande échelle, préférez Redis ou une file comme Kafka pour la durabilité et le partage entre workers.
Le paramètre pingback fonctionne-t-il derrière un pare-feu ou un proxy ?
Votre endpoint doit rester joignable publiquement par CaptchaAI. Derrière un pare-feu, exposez-le via un reverse proxy ou une URL dédiée, sinon restez sur le polling. Côté RGPD, journalisez le minimum et évitez les données personnelles dans les payloads.
Articles connexes
- Modèles de retry et de gestion d'erreurs en Python
- Sécuriser et valider vos callbacks webhook
- La référence des codes d'erreur CaptchaAI
Prochaines étapes
Fiabilisez la livraison de vos résolutions CAPTCHA : récupérez votre clé API CaptchaAI et déployez ces trois modèles dans votre pipeline.
Guides associés :