Tutorials

Erstellen eines CAPTCHA Solve Event Bus mit Node.js und CaptchaAI

Ein Event Bus ist die richtige Architektur, sobald mehrere Teile Ihrer Anwendung auf denselben CAPTCHA-Vorgang reagieren müssen – Logging, Metriken und Wiederholungslogik, ohne dass eine Komponente die andere kennt. Statt nur ein Ergebnis zurückzugeben, sendet der Bus jeden Zustand im Lebenszyklus als Ereignis: übermittelt, ausstehend, gelöst, fehlgeschlagen, Timeout.

Rückrufe und reines Polling liefern zwar das Endergebnis, geben Ihnen aber keinen Einblick in die Zwischenzustände und koppeln die auswertende Logik eng an den Aufruf. Genau diese Lücke schließt ein EventEmitter-basierter Bus in Node.js: Er entkoppelt das Lösen vom Reagieren und macht den gesamten Ablauf beobachtbar.

Die Architektur im Überblick

[CaptchaBus]
   ├── emit("submitted", { taskId, type, pageurl })
   ├── emit("pending", { taskId, elapsed })
   ├── emit("solved", { taskId, solution, duration })
   ├── emit("failed", { taskId, error, duration })
   └── emit("timeout", { taskId, elapsed })
        ↓          ↓           ↓
   [Logger]    [Metrics]   [Retry Handler]

Jeder Listener registriert sich eigenständig auf die Ereignisse, die ihn interessieren. Eine neue Funktion – etwa das Sammeln von Metriken – hängt sich einfach an das passende Ereignis an, ganz ohne Eingriff in den Code, der die CAPTCHAs tatsächlich löst. So bleibt die Kernlogik schlank, während Beobachtbarkeit und Nebenfunktionen frei wachsen können.

Die CaptchaBus-Klasse in JavaScript

Die Klasse erweitert den eingebauten EventEmitter von Node.js. Die Methode submit reicht die Aufgabe bei CaptchaAI ein, _poll fragt das Ergebnis in festen Intervallen ab, und an jedem Übergang wird das entsprechende Ereignis gesendet. Der Aufrufer erhält sofort eine taskId zurück und wartet nirgends blockierend.

const EventEmitter = require("events");
const axios = require("axios");

class CaptchaBus extends EventEmitter {
  constructor(apiKey, options = {}) {
    super();
    this.apiKey = apiKey;
    this.pollInterval = options.pollInterval || 5000;
    this.maxWait = options.maxWait || 300000; // 5 minutes
    this.pending = new Map();
  }

  async submit(params) {
    const { method, sitekey, pageurl, ...extra } = params;
    const taskId = `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;

    const submitParams = {
      key: this.apiKey,
      method: method || "userrecaptcha",
      googlekey: sitekey,
      pageurl: pageurl,
      json: 1,
      ...extra,
    };

    try {
      const resp = await axios.post(
        "https://ocr.captchaai.com/in.php",
        null,
        { params: submitParams }
      );

      if (resp.data.status !== 1) {
        this.emit("failed", {
          taskId,
          error: resp.data.request,
          duration: 0,
        });
        return null;
      }

      const captchaId = resp.data.request;
      const startTime = Date.now();

      this.emit("submitted", {
        taskId,
        captchaId,
        method: method || "userrecaptcha",
        pageurl,
      });

      // Start polling
      this._poll(taskId, captchaId, startTime);
      return taskId;
    } catch (err) {
      this.emit("failed", { taskId, error: err.message, duration: 0 });
      return null;
    }
  }

  async _poll(taskId, captchaId, startTime) {
    const check = async () => {
      const elapsed = Date.now() - startTime;

      if (elapsed > this.maxWait) {
        this.emit("timeout", { taskId, elapsed });
        return;
      }

      this.emit("pending", { taskId, elapsed });

      try {
        const resp = await axios.get("https://ocr.captchaai.com/res.php", {
          params: {
            key: this.apiKey,
            action: "get",
            id: captchaId,
            json: 1,
          },
        });

        if (resp.data.status === 1) {
          this.emit("solved", {
            taskId,
            captchaId,
            solution: resp.data.request,
            duration: Date.now() - startTime,
          });
        } else if (resp.data.request === "CAPCHA_NOT_READY") {
          setTimeout(check, this.pollInterval);
        } else {
          this.emit("failed", {
            taskId,
            error: resp.data.request,
            duration: Date.now() - startTime,
          });
        }
      } catch (err) {
        this.emit("failed", {
          taskId,
          error: err.message,
          duration: Date.now() - startTime,
        });
      }
    };

    setTimeout(check, this.pollInterval);
  }
}

module.exports = CaptchaBus;

Listener registrieren: Logging und Metriken

Weil der Bus ein EventEmitter ist, hängen Sie beliebig viele Listener an dasselbe Ereignis. Hier läuft ein Listener für lesbare Konsolenausgaben und ein zweiter, der parallel Kennzahlen zusammenzählt – beide voneinander unabhängig. Der Zähler für Metriken weiß nichts vom Logging und umgekehrt.

const CaptchaBus = require("./captcha-bus");

const bus = new CaptchaBus(process.env.CAPTCHAAI_API_KEY, {
  pollInterval: 5000,
  maxWait: 120000,
});

// Logging listener
bus.on("submitted", (e) => {
  console.log(`[SUBMIT] ${e.taskId} → ${e.method} on ${e.pageurl}`);
});

bus.on("pending", (e) => {
  console.log(`[PENDING] ${e.taskId} — ${(e.elapsed / 1000).toFixed(1)}s`);
});

bus.on("solved", (e) => {
  console.log(
    `[SOLVED] ${e.taskId} in ${(e.duration / 1000).toFixed(1)}s — ${e.solution.substring(0, 30)}...`
  );
});

bus.on("failed", (e) => {
  console.error(`[FAILED] ${e.taskId} — ${e.error}`);
});

bus.on("timeout", (e) => {
  console.error(
    `[TIMEOUT] ${e.taskId} after ${(e.elapsed / 1000).toFixed(1)}s`
  );
});

// Metrics listener
const metrics = { submitted: 0, solved: 0, failed: 0, totalDuration: 0 };

bus.on("submitted", () => metrics.submitted++);
bus.on("solved", (e) => {
  metrics.solved++;
  metrics.totalDuration += e.duration;
});
bus.on("failed", () => metrics.failed++);

// Submit a CAPTCHA
bus.submit({
  sitekey: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
  pageurl: "https://example.com",
});

Dasselbe Muster in Python

Python bringt keinen EventEmitter mit, das Muster lässt sich aber mit einem defaultdict aus Listener-Listen in wenigen Zeilen nachbauen. Das Polling läuft hier in einem Hintergrund-Thread, damit submit genauso nicht-blockierend zurückkehrt wie in der Node.js-Variante.

import os
import time
import threading
from collections import defaultdict
import requests


class CaptchaBus:
    def __init__(self, api_key, poll_interval=5, max_wait=300):
        self.api_key = api_key
        self.poll_interval = poll_interval
        self.max_wait = max_wait
        self._listeners = defaultdict(list)

    def on(self, event, callback):
        """Register a listener for an event."""
        self._listeners[event].append(callback)
        return self

    def emit(self, event, data):
        """Emit an event to all registered listeners."""
        for callback in self._listeners.get(event, []):
            try:
                callback(data)
            except Exception as e:
                print(f"Listener error on {event}: {e}")

    def submit(self, sitekey, pageurl, method="userrecaptcha", **extra):
        """Submit a CAPTCHA and begin tracking."""
        task_id = f"task_{int(time.time())}_{id(sitekey) % 10000}"

        resp = requests.post("https://ocr.captchaai.com/in.php", data={
            "key": self.api_key,
            "method": method,
            "googlekey": sitekey,
            "pageurl": pageurl,
            "json": 1,
            **extra
        })
        data = resp.json()

        if data.get("status") != 1:
            self.emit("failed", {
                "task_id": task_id,
                "error": data.get("request"),
                "duration": 0
            })
            return None

        captcha_id = data["request"]
        start_time = time.time()

        self.emit("submitted", {
            "task_id": task_id,
            "captcha_id": captcha_id,
            "method": method,
            "pageurl": pageurl
        })

        # Poll in a background thread
        thread = threading.Thread(
            target=self._poll,
            args=(task_id, captcha_id, start_time),
            daemon=True
        )
        thread.start()
        return task_id

    def _poll(self, task_id, captcha_id, start_time):
        while True:
            elapsed = time.time() - start_time

            if elapsed > self.max_wait:
                self.emit("timeout", {"task_id": task_id, "elapsed": elapsed})
                return

            time.sleep(self.poll_interval)
            self.emit("pending", {"task_id": task_id, "elapsed": elapsed})

            resp = requests.get("https://ocr.captchaai.com/res.php", params={
                "key": self.api_key,
                "action": "get",
                "id": captcha_id,
                "json": 1
            })
            data = resp.json()

            if data.get("status") == 1:
                self.emit("solved", {
                    "task_id": task_id,
                    "solution": data["request"],
                    "duration": time.time() - start_time
                })
                return
            elif data.get("request") != "CAPCHA_NOT_READY":
                self.emit("failed", {
                    "task_id": task_id,
                    "error": data.get("request"),
                    "duration": time.time() - start_time
                })
                return


# Usage
bus = CaptchaBus(os.environ["CAPTCHAAI_API_KEY"])

bus.on("submitted", lambda e: print(f"[SUBMIT] {e['task_id']}"))
bus.on("solved", lambda e: print(f"[SOLVED] {e['task_id']} in {e['duration']:.1f}s"))
bus.on("failed", lambda e: print(f"[FAILED] {e['task_id']} — {e['error']}"))
bus.on("timeout", lambda e: print(f"[TIMEOUT] {e['task_id']}"))

bus.submit("6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-", "https://example.com")

Fehlgeschlagene Aufgaben automatisch wiederholen

Weil die Wiederholungslogik selbst nur ein Listener auf failed ist, bleibt sie sauber vom Lösungscode getrennt. Ein Zähler begrenzt die Versuche, danach gibt der Handler auf – so vermeiden Sie Endlosschleifen bei dauerhaft fehlerhaften Parametern.

// Automatic retry on failure
bus.on("failed", async (e) => {
  if (e.retryCount >= 3) {
    console.error(`[GIVE UP] ${e.taskId} after 3 retries`);
    return;
  }

  console.log(`[RETRY] ${e.taskId} — attempt ${(e.retryCount || 0) + 1}`);
  await bus.submit({
    ...e.originalParams,
    _retryCount: (e.retryCount || 0) + 1,
  });
});

Promise-Wrapper für async/await

Manchmal möchten Sie eine einzelne Lösung einfach awaiten, statt Listener zu verdrahten. Legen Sie dafür eine Promise-basierte API über den Event Bus – der Wrapper räumt seine Listener nach solved, failed oder timeout selbst wieder auf.

function solveCaptcha(bus, params) {
  return new Promise((resolve, reject) => {
    const taskId = bus.submit(params);

    function onSolved(e) {
      if (e.taskId === taskId) {
        cleanup();
        resolve(e.solution);
      }
    }

    function onFailed(e) {
      if (e.taskId === taskId) {
        cleanup();
        reject(new Error(e.error));
      }
    }

    function cleanup() {
      bus.removeListener("solved", onSolved);
      bus.removeListener("failed", onFailed);
      bus.removeListener("timeout", onFailed);
    }

    bus.on("solved", onSolved);
    bus.on("failed", onFailed);
    bus.on("timeout", onFailed);
  });
}

// Usage
const solution = await solveCaptcha(bus, {
  sitekey: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
  pageurl: "https://example.com",
});

Der Event Bus im Produktivbetrieb

Im Dauerbetrieb läuft ein solcher Bus typischerweise in einem Worker-Prozess auf einem VPS – bei DACH-Teams oft auf Hetzner, IONOS oder netcup. Da CaptchaAI pro Thread abrechnet und nicht pro Lösung, bestimmt Ihre gebuchte Thread-Zahl, wie viele CAPTCHAs gleichzeitig durch den Bus laufen können; schon der Einstiegstarif BASIC (15 $/Monat, 5 Threads) erlaubt fünf parallele Vorgänge, größere Tarife entsprechend mehr. Das solved-Ereignis eignet sich gut, um die Durchlaufzeit an ein GitLab-CI-Dashboard oder Prometheus zu melden.

Wenn Ihre Automatisierung dabei Daten von Drittseiten einsammelt, beachten Sie den DSGVO-Kontext: IP-Adressen gelten als personenbezogene Daten. Prüfen Sie Ihre Datenflüsse und die Rechtsgrundlage, bevor Sie Scraping-Workloads in Produktion nehmen – das ist Sorgfaltspflicht auf Ihrer Seite, keine Aussage über CaptchaAI.

Fehlerbehebung

Problem Ursache Lösung
Ein Listener reagiert nicht Ereignisname vertippt (z. B. „solve" statt „solved") Gleichen Sie die Namen in emit und on exakt ab
Warnung wegen Speicherleck Zu viele Listener auf einem Ereignis Mit setMaxListeners() anheben oder Listener nach Gebrauch entfernen
Token wird erzeugt, aber vom Ziel abgelehnt sitekey, pageurl oder Session-Kontext passen nicht zusammen Parameter erneut erfassen und das Token in derselben Browser- oder HTTP-Sitzung einsetzen
Polling läuft ständig ins Timeout Intervall oder Wartezeit zu eng gesetzt Alle 5–10 Sekunden abfragen und Timeout von echten Fehlercodes trennen
Beispiel läuft lokal, im Workflow aber nicht Token wird nicht ins richtige Formularfeld der Zielkette eingetragen Übergabepfad vom Solver bis zur finalen Zielanfrage prüfen

Häufige Fragen

Wie viele CAPTCHAs kann ich parallel über den Bus lösen?

So viele, wie Ihr Tarif an Threads erlaubt. CaptchaAI rechnet pro gleichzeitigem Thread ab, nicht pro Lösung – ein Thread verarbeitet ein CAPTCHA und ist danach sofort wieder frei. BASIC (15 $/Monat) umfasst 5 Threads, ADVANCE (90 $/Monat) 50 Threads. Der Bus selbst begrenzt nichts; die Obergrenze ist Ihre Thread-Zuteilung.

Was passiert, wenn ein Token nach dem Lösen abläuft?

Setzen Sie das Token unmittelbar nach dem solved-Ereignis ein. Lösungstoken sind kurzlebig – bei reCAPTCHA rund 120 Sekunden – und werden vom Ziel abgelehnt, sobald sie verfallen sind. Verarbeiten Sie die Lösung deshalb direkt in der Zielanfrage, statt sie zwischenzuspeichern.

Wann sollte ich auf einen externen Message-Broker umsteigen?

Bei einem einzelnen Prozess ist der eingebaute EventEmitter einfacher und schneller. Sobald mehrere Prozesse oder Services auf dieselben CAPTCHA-Ereignisse reagieren müssen, greifen Sie zu Redis, RabbitMQ oder Kafka. Für die meisten In-Process-Pipelines bleibt der leichtgewichtige Bus die passende Wahl.

Unterstützt der Bus auch reCAPTCHA v3, Turnstile und GeeTest v3?

Ja. Die Architektur ist typunabhängig – Sie ändern nur den method-Parameter (userrecaptcha, turnstile, geetest) und die zugehörigen Felder. Der Lebenszyklus aus übermittelt, ausstehend, gelöst und fehlgeschlagen bleibt für alle unterstützten CAPTCHA-Typen identisch.


Verwandte Leitfäden

Kommentare sind für diesen Artikel deaktiviert.