Tutorials

CaptchaAI Callback-URL-Fehlerbehandlung: Wiederholungsversuche und Muster für unzustellbare Nachrichten

Ein Callback ist keine Zustellgarantie. Sobald CaptchaAI das Ergebnis per Pingback an Ihre Callback-URL schickt, hängt die Zustellung davon ab, dass Ihr Server erreichbar ist, schnell genug antwortet und den Datensatz auch tatsächlich speichert. Fällt nur eine dieser Bedingungen aus, ist die Lösung weg – der Thread hat sie aber trotzdem berechnet. Die zuverlässige Antwort lautet: Behandeln Sie den Callback als schnellen Normalfall und legen Sie darunter ein zweites Sicherungsnetz.

Dieses Tutorial zeigt drei Muster, die genau das leisten, ohne dass eine einzige Lösung verloren geht:

  • Fallback-Polling fragt jede Aufgabe aktiv ab, für die innerhalb eines Timeouts kein Callback eingetroffen ist.
  • Eine Dead-Letter-Queue fängt Lösungen auf, deren Verarbeitung im Handler scheitert.
  • Ein idempotenter Handler verarbeitet doppelte Zustellungen genau einmal.

Welche Fehler bei der Callback-Zustellung auftreten

Fehlermodus Symptom Folge
Server nicht erreichbar CaptchaAI erhält „connection refused" Lösung wird nicht zugestellt
Server antwortet mit 5xx CaptchaAI bekommt eine Fehlerantwort Kein erneuter Versuch garantiert (implementierungsabhängig)
Netzwerk-Timeout Die Verbindung von CaptchaAI hängt Lösung geht möglicherweise verloren
Handler stürzt ab Anfrage angenommen, Ergebnis aber nicht gespeichert Lösung wird stillschweigend verworfen

Die Konsequenz ist in allen vier Fällen dieselbe: Verlassen Sie sich nie allein auf den Callback. Sehen Sie immer einen Fallback vor.

Das ist kein rein akademisches Risiko. CaptchaAI rechnet pro Thread ab, nicht pro Lösung – ein belegter Thread hat die Abfrage bereits gelöst, selbst wenn das Ergebnis nie bei Ihnen ankommt. Schon der Einstiegstarif BASIC (15 $/Monat, 5 Threads) verarbeitet dauerhaft parallele Aufgaben; jede verlorene Zustellung bedeutet, dass ein Workflow ohne Not erneut anstößt. Robuste Fehlerbehandlung schützt also nicht nur Ihre Daten, sondern auch den Durchsatz, den Sie bezahlen.

Muster 1: Callback mit Fallback-Polling

Der stabilste Ansatz kombiniert beide Zustellwege: Sie nehmen Callbacks an, sobald sie eintreffen, und fragen zusätzlich alle Aufgaben aktiv ab, für die innerhalb eines Timeouts kein Callback kam. Der Callback bleibt der schnelle Normalweg, das Polling fängt jede verpasste Zustellung auf.

So greifen beide Wege ineinander:

  1. Sie übermitteln die Aufgabe mit pingback-Parameter und vermerken sie lokal als offen.
  2. Trifft der Callback ein, speichern Sie das Ergebnis und streichen die Aufgabe aus der Pending-Liste.
  3. Bleibt der Callback über das Timeout hinaus aus, holt der Poller das Ergebnis über res.php nach.

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);

Wählen Sie das Timeout großzügig: Zwei Minuten (120 Sekunden) decken selbst langsame Lösungen plus die Latenz der Callback-Zustellung ab. Erst danach lohnt sich eine aktive Abfrage über res.php.

Muster 2: Dead-Letter-Queue für fehlgeschlagene Verarbeitung

Manchmal kommt der Callback an, aber die Verarbeitung scheitert: Die Datenbank ist gerade nicht erreichbar, eine Validierung schlägt fehl, ein nachgelagerter Dienst antwortet nicht. Statt die Lösung in diesem Moment zu verlieren, schreiben Sie sie in eine Dead-Letter-Queue (DLQ) und arbeiten sie später erneut ab. Wichtig: Bestätigen Sie CaptchaAI den Empfang trotzdem mit 200 – der Fehler liegt auf Ihrer Seite, nicht bei der Zustellung.

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);

Der Reprozessor läuft dabei getrennt vom Callback-Handler – etwa als eigener Worker auf einem Hetzner- oder netcup-Server oder als geplanter Job in GitLab CI – und leert die Queue, sobald der zugrunde liegende Dienst wieder verfügbar ist.

DSGVO-Hinweis: Enthält der in der DLQ gespeicherte Datensatz personenbezogene Daten aus Ihrem Workflow, gelten dafür die üblichen DSGVO-Pflichten. Legen Sie Aufbewahrungsdauer und Löschkonzept für Ihre DLQ-Dateien fest, bevor der erste Datensatz auf der Platte landet.

Muster 3: Idempotenter Callback-Handler

Callbacks können mehrfach zugestellt werden – etwa wenn Ihr Fallback-Polling eine Aufgabe abholt, die parallel doch noch per Callback eintrifft. Ein idempotenter Handler verarbeitet jede Task-ID nur einmal und ignoriert Wiederholungen still:

@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

Zwei Punkte entscheiden hier über die Korrektheit:

  • Die Prüfung läuft innerhalb desselben Locks wie das Schreiben – sonst öffnet sich genau die Race Condition wieder, die der Handler eigentlich schließen soll.
  • Auch bei einer erkannten Wiederholung antwortet der Handler mit 200; für CaptchaAI ist die Zustellung damit sauber quittiert und wird nicht erneut versucht.

Welches Muster passt zu welchem Setup

Szenario Empfohlenes Muster
Geringes Volumen, seltene Ausfälle Callback + Fallback-Polling
Hohes Volumen, mögliche Datenbankausfälle Dead-Letter-Queue
Mehrere Konsumenten verarbeiten dasselbe Ergebnis Idempotenter Handler
Produktivsystem mit SLAs Alle drei kombiniert

Typische Probleme beim Debugging

Problem Ursache Lösung
Das Fallback-Polling findet bereits zugestellte Aufgaben Race Condition zwischen Callback und Poller Idempotenzprüfung ergänzen – überspringen, wenn die Task-ID schon in results liegt
Die DLQ wächst, wird aber nicht geleert Reprozessor läuft nicht oder scheitert wiederholt Logs des Reprozessors prüfen und sicherstellen, dass die Ursache (z. B. die Datenbank) behoben ist
Callback antwortet mit 200, das Ergebnis fehlt trotzdem Der Handler stürzt nach dem Senden der Antwort ab Erst verarbeiten, dann antworten – oder das DLQ-Muster einsetzen
Zu viele Fallback-Abfragen Zu viele Aufgaben laufen ins Timeout Timeout-Schwelle erhöhen und die Serververfügbarkeit prüfen

Häufige Fragen

Wie unterscheide ich einen abgelaufenen Token von einem echten Fehlercode?

Ein abgelaufener Token wird erst von der Zielanwendung abgelehnt, während res.php weiterhin status: 1 mit einem gültigen Ergebnis meldet. Ein echter Fehler kommt dagegen als eigener Code zurück – jeder Wert außer CAPCHA_NOT_READY. Trennen Sie im Poller beide Fälle: status: 1 ist ein Ergebnis, alles andere jenseits von CAPCHA_NOT_READY ist ein permanenter Fehler und gehört aus der Pending-Liste entfernt.

Wie lange bleibt ein CAPTCHA-Token gültig?

In der Regel etwa 120 Sekunden. reCAPTCHA-Token laufen nach rund zwei Minuten ab, deshalb sollten Sie sie erst unmittelbar vor der Übermittlung an die Zielseite einsetzen. An dieser Frist orientiert sich auch das Timeout im Fallback-Polling.

Muss ich meinen Callback-Endpunkt absichern?

Ja. Ihre Callback-URL ist öffentlich erreichbar. Validieren Sie eingehende Anfragen über einen schwer zu erratenden Pfad, ein Shared Secret oder eine Signaturprüfung, damit niemand gefälschte Ergebnisse einschleust oder Ihren Handler mit leeren Aufrufen belastet.

Was passiert mit laufenden Aufgaben, wenn mein Server neu startet?

Ohne Persistenz gehen sie verloren, weil pending_tasks und results im Arbeitsspeicher liegen. Für Produktivsysteme legen Sie den Zustand in einen externen Speicher wie Redis oder eine Datenbank. Nach einem Deploy oder Neustart übernimmt dann das Fallback-Polling die offenen Aufgaben und arbeitet sie sauber ab.


Verwandte Leitfäden

Kommentare sind für diesen Artikel deaktiviert.