Tutoriels

Gestion des erreurs d'URL de rappel CaptchaAI : modèles de nouvelle tentative et de lettre morte

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

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 :

Les commentaires sont désactivés pour cet article.