Fehlerbehebung

Umgang mit reCAPTCHA v2 und Cloudflare Turnstile auf derselben Site

Zwei Läufe gegen dieselbe Domain, zwei Ergebnisse: Der Login wird sauber gelöst, der Checkout läuft ins Timeout. Der Grund steht meist eine Zeile über dem Fehler – method ist fest auf userrecaptcha gesetzt, ausgeliefert wird an dieser Stelle aber Cloudflare Turnstile. Die Korrektur ist unspektakulär, aber strukturell: Bestimmen Sie den Anbieter bei jedem Seitenaufruf neu und leiten Sie Methode, Sitekey-Parameter und Antwortfeld daraus ab. Dieser Leitfaden zeigt die Marker beider Anbieter im Quelltext, eine fertige Erkennungsroutine in Python und JavaScript sowie die typischen Fehlerbilder.

reCAPTCHA v2 oder Turnstile: Anbieter im Quelltext erkennen

Beide Widgets tragen ihren Sitekey im Attribut data-sitekey – dieses Attribut allein sagt also nichts über den Anbieter aus. Eindeutig wird es erst über die CSS-Klasse und die nachgeladene Skript-URL.

Anbieter HTML-Marker Skript-URL Antwortfeld
reCAPTCHA v2 class="g-recaptcha" google.com/recaptcha/api.js g-recaptcha-response
Cloudflare Turnstile class="cf-turnstile" challenges.cloudflare.com/turnstile cf-turnstile-response
hCaptcha class="h-captcha" js.hcaptcha.com/1/api.js h-captcha-response

Zwei Hinweise zur Tabelle: Prüfen Sie immer zuerst auf cf-turnstile und erst danach auf g-recaptcha, denn ein einzelnes data-sitekey ohne Klassenkontext führt regelmäßig zur falschen Zuordnung. Die hCaptcha-Zeile wiederum steht hier ausschließlich zur Abgrenzung: CaptchaAI unterstützt hCaptcha nicht, aber wer den Marker sauber erkennt, merkt früh, dass dieser Pfad im Workflow gesondert behandelt oder kontrolliert abgebrochen werden muss.

Warum eine Website zwei Anbieter parallel ausliefert

Szenario Wie es sich im Traffic zeigt
Getrennte Seitenbereiche Login mit reCAPTCHA v2, Checkout mit Turnstile
A/B-Test zweier Anbieter Dieselbe URL zeigt je Session zufällig den einen oder den anderen Typ
Laufende Migration Alte Templates tragen reCAPTCHA, neu ausgerollte Seiten Turnstile
Fallback-Kette Der primäre Anbieter lädt nicht, das Frontend blendet den zweiten ein
Regionale Ausspielung Besucher aus den USA sehen reCAPTCHA, Besucher aus der EU Turnstile

Ein Fall aus dem DACH-Alltag

Ein Shopware-Shop, gehostet bei Hetzner, stellt den Checkout auf Cloudflare Turnstile um, nachdem der Datenschutzbeauftragte den Einsatz von Google-Diensten im Bestellprozess hinterfragt hat. Der Login-Bereich bleibt vorerst bei reCAPTCHA v2, weil dort ein älteres Theme im Einsatz ist. Die End-to-End-Tests des eigenen QA-Teams kennen nur userrecaptcha – und brechen ab dem ersten Deployment im Checkout ab, obwohl an der Testlogik nichts geändert wurde.

Dieser Zwischenzustand ist der Normalfall: Staging und Produktion machen solche Umstellungen selten am selben Tag mit; in der GitLab-CI-Pipeline fällt der Bruch erst im roten Job auf. Eine dynamische Erkennung überbrückt die Migrationsphase, ohne doppelten Testcode. Nebenbei gilt: Sobald Proxys oder Logs mit IP-Adressen im Spiel sind, gehören Datenflüsse und Rechtsgrundlage nach DSGVO in dieselbe Prüfung – IP-Adressen zählen als personenbezogene Daten.

reCAPTCHA v2 und Turnstile in Python erkennen und lösen

Die folgende Routine ruft die Seite ab, prüft zuerst auf Turnstile, dann auf reCAPTCHA v2 – inklusive der per grecaptcha.render() nachgeladenen Variante – und liefert ein CaptchaInfo-Objekt zurück, das Methode, Sitekey und Antwortfeld zusammenhält. solve_captcha() übermittelt die Aufgabe an in.php und fragt das Ergebnis anschließend im Fünf-Sekunden-Takt über res.php ab.

import requests
import time
import re
from dataclasses import dataclass

API_KEY = "YOUR_API_KEY"
SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"


@dataclass
class CaptchaInfo:
    provider: str     # "recaptcha" or "turnstile"
    method: str       # API method name
    sitekey: str
    pageurl: str
    response_field: str  # Form field name for the token


def detect_captcha_type(html, pageurl):
    """
    Detect which CAPTCHA provider is on the page.
    Returns CaptchaInfo or None.
    """
    # Check for Turnstile
    turnstile_match = re.search(
        r'class=["\'][^"\']*cf-turnstile[^"\']*["\'][^>]*data-sitekey=["\']([^"\']+)["\']',
        html,
    )
    if not turnstile_match:
        turnstile_match = re.search(
            r'data-sitekey=["\']([^"\']+)["\'][^>]*class=["\'][^"\']*cf-turnstile',
            html,
        )

    if turnstile_match:
        return CaptchaInfo(
            provider="turnstile",
            method="turnstile",
            sitekey=turnstile_match.group(1),
            pageurl=pageurl,
            response_field="cf-turnstile-response",
        )

    # Check for reCAPTCHA
    recaptcha_match = re.search(
        r'class=["\'][^"\']*g-recaptcha[^"\']*["\'][^>]*data-sitekey=["\']([^"\']+)["\']',
        html,
    )
    if not recaptcha_match:
        recaptcha_match = re.search(
            r'data-sitekey=["\']([^"\']+)["\'][^>]*class=["\'][^"\']*g-recaptcha',
            html,
        )

    # Also check for script-rendered reCAPTCHA
    if not recaptcha_match:
        recaptcha_match = re.search(
            r'grecaptcha\.render\([^,]+,\s*\{[^}]*["\']sitekey["\']\s*:\s*["\']([^"\']+)["\']',
            html,
        )

    if recaptcha_match:
        return CaptchaInfo(
            provider="recaptcha",
            method="userrecaptcha",
            sitekey=recaptcha_match.group(1),
            pageurl=pageurl,
            response_field="g-recaptcha-response",
        )

    return None


def solve_captcha(info):
    """Solve any detected CAPTCHA type via CaptchaAI."""
    params = {
        "key": API_KEY,
        "method": info.method,
        "json": 1,
    }

    if info.method == "userrecaptcha":
        params["googlekey"] = info.sitekey
        params["pageurl"] = info.pageurl
    elif info.method == "turnstile":
        params["sitekey"] = info.sitekey
        params["pageurl"] = info.pageurl

    resp = requests.post(SUBMIT_URL, data=params, timeout=30).json()
    if resp.get("status") != 1:
        raise RuntimeError(f"Submit failed: {resp.get('request')}")

    task_id = resp["request"]
    for _ in range(60):
        time.sleep(5)
        poll = requests.get(RESULT_URL, params={
            "key": API_KEY, "action": "get",
            "id": task_id, "json": 1,
        }, timeout=15).json()

        if poll.get("request") == "CAPCHA_NOT_READY":
            continue
        if poll.get("status") == 1:
            return poll["request"]
        raise RuntimeError(f"Solve failed: {poll.get('request')}")

    raise RuntimeError("Timeout")


def process_page(session, url):
    """Fetch page, detect CAPTCHA type, solve, and return form-ready data."""
    response = session.get(url)
    captcha_info = detect_captcha_type(response.text, url)

    if not captcha_info:
        print(f"No CAPTCHA detected on {url}")
        return None

    print(f"Detected {captcha_info.provider} on {url}")
    print(f"  Sitekey: {captcha_info.sitekey[:30]}...")

    token = solve_captcha(captcha_info)
    print(f"  Solved: {token[:30]}...")

    return {
        "provider": captcha_info.provider,
        "response_field": captcha_info.response_field,
        "token": token,
    }


# Usage: Handle multiple pages with different providers
session = requests.Session()

pages = [
    "https://example.com/login",      # Might have reCAPTCHA
    "https://example.com/checkout",   # Might have Turnstile
]

for url in pages:
    result = process_page(session, url)
    if result:
        form_data = {result["response_field"]: result["token"]}
        # Add other form fields...
        # session.post(url, data=form_data)

Der Kern ist die Zuordnung am Ende: Aus dem erkannten Anbieter ergeben sich drei Werte, die zusammenpassen müssen.

Anbieter method Sitekey-Parameter Antwortfeld Lösungszeit (Obergrenze)
reCAPTCHA v2 userrecaptcha googlekey g-recaptcha-response < 60 s
Cloudflare Turnstile turnstile sitekey cf-turnstile-response < 10 s

Die Zeitangaben stammen von den öffentlichen Solver-Seiten; sie markieren die obere Grenze, nicht den Durchschnitt. Für die Praxis heißt das vor allem: Ein Turnstile-Lauf gibt seinen Thread deutlich früher wieder frei als ein reCAPTCHA-v2-Lauf.

Dieselbe Logik in JavaScript

In Node.js bleibt der Ablauf identisch, nur kompakter: detectCaptchaType() liefert ein Objekt mit method, sitekey und field, solveCaptcha() übernimmt Übermittlung und Polling.

const API_KEY = "YOUR_API_KEY";
const SUBMIT_URL = "https://ocr.captchaai.com/in.php";
const RESULT_URL = "https://ocr.captchaai.com/res.php";

function detectCaptchaType(html, pageurl) {
  // Turnstile
  const turnstileMatch = html.match(/cf-turnstile[^>]*data-sitekey=["']([^"']+)["']/);
  if (turnstileMatch) {
    return { provider: "turnstile", method: "turnstile", sitekey: turnstileMatch[1], pageurl, field: "cf-turnstile-response" };
  }

  // reCAPTCHA
  const recaptchaMatch = html.match(/g-recaptcha[^>]*data-sitekey=["']([^"']+)["']/);
  if (recaptchaMatch) {
    return { provider: "recaptcha", method: "userrecaptcha", sitekey: recaptchaMatch[1], pageurl, field: "g-recaptcha-response" };
  }

  // Script-rendered reCAPTCHA
  const scriptMatch = html.match(/sitekey["']\s*:\s*["']([^"']+)["']/);
  if (scriptMatch) {
    return { provider: "recaptcha", method: "userrecaptcha", sitekey: scriptMatch[1], pageurl, field: "g-recaptcha-response" };
  }

  return null;
}

async function solveCaptcha(info) {
  const body = new URLSearchParams({ key: API_KEY, method: info.method, json: "1" });
  if (info.method === "userrecaptcha") { body.set("googlekey", info.sitekey); body.set("pageurl", info.pageurl); }
  else if (info.method === "turnstile") { body.set("sitekey", info.sitekey); body.set("pageurl", info.pageurl); }

  const resp = await (await fetch(SUBMIT_URL, { method: "POST", body })).json();
  if (resp.status !== 1) throw new Error(`Submit: ${resp.request}`);

  const taskId = resp.request;
  for (let i = 0; i < 60; i++) {
    await new Promise((r) => setTimeout(r, 5000));
    const url = `${RESULT_URL}?key=${API_KEY}&action=get&id=${taskId}&json=1`;
    const poll = await (await fetch(url)).json();
    if (poll.request === "CAPCHA_NOT_READY") continue;
    if (poll.status === 1) return poll.request;
    throw new Error(`Solve: ${poll.request}`);
  }
  throw new Error("Timeout");
}

async function processPage(url) {
  const response = await fetch(url);
  const html = await response.text();
  const info = detectCaptchaType(html, url);

  if (!info) { console.log(`No CAPTCHA on ${url}`); return null; }
  console.log(`${info.provider} detected on ${url}`);

  const token = await solveCaptcha(info);
  return { provider: info.provider, field: info.field, token };
}

// Usage
const pages = ["https://example.com/login", "https://example.com/checkout"];
for (const url of pages) {
  const result = await processPage(url);
  if (result) {
    console.log(`Solved ${result.provider}: ${result.token.substring(0, 30)}...`);
  }
}

Bei serverseitig geliefertem HTML genügt der einfache fetch()-Aufruf. Wird das Widget erst im Browser eingehängt, holen Sie den Quelltext aus einer Playwright- oder Puppeteer-Sitzung – die Erkennungsfunktion selbst bleibt unverändert.

Fehlerbehebung

Symptom Ursache Vorgehen
Falscher Typ erkannt Die Regex greift auf data-sitekey zu, bevor sie die Anbieterklasse geprüft hat Erst cf-turnstile bzw. g-recaptcha matchen, dann den Sitekey auslesen
Token wird abgelehnt, obwohl die Lösung ankam Das Token steht im Feld des falschen Anbieters Antwortfeld strikt aus der Erkennung ableiten: cf-turnstile-response oder g-recaptcha-response
Der Typ wechselt zwischen zwei Aufrufen A/B-Test oder regionale Ausspielung Pro Request neu erkennen und den Anbieter nie aus dem ersten Besuch zwischenspeichern
Beide Widgets im Quelltext Eines ist ausgeblendet oder inaktiv Sichtbarkeit und gefüllten data-sitekey prüfen, nur das aktive Widget lösen
Erkennung findet nichts Das Widget wird per Skript nachgeladen Auf grecaptcha.render() bzw. turnstile.render() prüfen oder die gerenderte Seite auswerten
Test läuft, Produktion scheitert Session, Header oder Proxy-Kontext weichen vom Test ab Erfolgreiche Testbedingungen unverändert übernehmen und Solver-Typ, Latenz, Fehlercode gemeinsam protokollieren

Was die Doppelabdeckung kostet

Zwei Anbieter bedeuten weder zwei Verträge noch zwei API-Schlüssel. CaptchaAI rechnet Thread-basiert ab: Sie zahlen für gleichzeitig laufende Lösungen, nicht pro gelöstem CAPTCHA, und die Zahl der Lösungen pro Thread ist im Monatstarif nicht gedeckelt. Ob ein Thread gerade userrecaptcha oder turnstile bearbeitet, ändert am Preis nichts.

Relevant wird der Anbietermix erst bei der Kapazitätsplanung, weil ein reCAPTCHA-v2-Lauf einen Thread länger belegt als ein Turnstile-Lauf. Für einzelne Testpipelines reicht BASIC (15 $/Monat, 5 Threads); wer parallele Scraping-Worker über mehrere Seitentypen fährt, landet eher bei ADVANCE (90 $/Monat, 50 Threads). Die Preise sind in US-Dollar ausgewiesen.

Häufige Fragen

Wie lange bleibt ein gelöstes Token gültig?

Kurz – planen Sie mit rund zwei Minuten. Ein reCAPTCHA-v2-Token verfällt nach etwa 120 Sekunden, Turnstile-Token sind Einmal-Token und werden serverseitig eingelöst. Lösen Sie deshalb erst unmittelbar vor dem Absenden des Formulars und legen Sie keine Token auf Vorrat an.

Brauche ich für beide Anbieter getrennte Zugangsdaten?

Nein. Ein API-Schlüssel, dieselben Endpunkte in.php und res.php, nur ein anderer Wert in method und ein anderer Parametername für den Sitekey. Genau deshalb lässt sich die Fallunterscheidung in eine einzige Funktion kapseln.

Was passiert mit der Laufzeit, wenn statt Turnstile plötzlich reCAPTCHA v2 kommt?

Sie steigt spürbar. Turnstile wird typischerweise in unter 10 Sekunden gelöst, reCAPTCHA v2 in unter 60 Sekunden. Wer knapp gesetzte Timeouts in der Pipeline hat, sollte sie am langsameren der beiden Typen ausrichten, nicht am schnelleren.

Muss die Erkennung im Headless-Browser laufen?

Nur wenn das Widget nicht im ausgelieferten HTML steht. Ein reiner HTTP-Request ist schneller; sobald das Formular erst clientseitig entsteht, brauchen Sie das gerenderte DOM aus Selenium, Playwright oder Puppeteer.

Was tun, wenn eine Seite auf hCaptcha umstellt?

Die Erkennung fängt den Fall sauber ab, gelöst wird er nicht: hCaptcha gehört nicht zu den von CaptchaAI unterstützten Typen. Behandeln Sie den Marker als eigenen Zweig, protokollieren Sie ihn und brechen Sie den Lauf kontrolliert ab, statt ihn in ein Timeout laufen zu lassen.


Verwandte Leitfäden

Kommentare sind für diesen Artikel deaktiviert.