Fehlerbehebung

reCAPTCHA: Domain-Fehler erkennen und beheben

Im Log steht "success": true, und Ihr Backend weist das Token trotzdem ab. Schuld ist dann selten der Solver, sondern die Domain: reCAPTCHA bindet jedes Token an den Hostnamen der Seite, auf der es entstanden ist. Weicht die pageurl auch nur um ein www. vom Ziel ab, sieht das Token gültig aus – die Serverprüfung fällt trotzdem negativ aus.

Drei Punkte erklären den Großteil dieser Fälle:

  • Der Hostname im Token stammt aus der Seite, auf der das Widget geladen wurde – nicht aus Ihrem HTTP-Client.
  • Die pageurl muss exakt der URL entsprechen, an die Sie das gelöste Token absenden.
  • Weiterleitungen, Login-Subdomains und regionale CDN-Hostnamen sind die häufigsten Auslöser.

Wie reCAPTCHA Token an eine Domain bindet

Über die Annahme entscheiden drei Stellen – nur eine liegt in Ihrer Hand.

Kontrollpunkt Was geprüft wird
Client-seitig Das Widget lädt nur auf freigegebenen Domains (abschaltbar)
Token-Erzeugung Der eingebettete Hostname entspricht dem Seitenursprung
Serverprüfung siteverify liefert den Hostnamen; der Server entscheidet über die Annahme

Der Ablauf im Überblick

Site owner registers reCAPTCHA → adds allowed domains (example.com, www.example.com)
    ↓
reCAPTCHA widget loads on example.com → matches allowed domain ✓
    ↓
Token generated with embedded hostname
    ↓
Server validates token via siteverify API
    ↓
Google checks: Does token hostname match allowed domains?
    ├─ YES → { "success": true, "hostname": "example.com" }
    └─ NO  → { "success": false, error or hostname mismatch }

Entscheidend ist die letzte Verzweigung: Google meldet den Hostnamen, aber ob er exakt passen muss, legt der Website-Betreiber fest.


Drei Fehlerbilder, die Sie im Log sehen

Fehler 1: Der Hostname in der siteverify-Antwort weicht ab

Das Token ist gültig, doch hostname zeigt eine andere Domain als erwartet:

{
    "success": true,
    "hostname": "subdomain.example.com",
    "challenge_ts": "2025-01-15T10:30:00Z"
}

Manche Server-Implementierungen lehnen genau das ab:

# Server-side validation that checks hostname
def validate_token(token, secret_key, expected_hostname):
    result = requests.post(
        "https://www.google.com/recaptcha/api/siteverify",
        data={"secret": secret_key, "response": token},
    ).json()

    if not result.get("success"):
        return False

    # This check causes failures when hostnames don't match
    if result.get("hostname") != expected_hostname:
        return False  # Domain mismatch!

    return True

Typisch sind drei Konstellationen: für www.example.com gelöst, gegen example.com validiert; auf staging.example.com gelöst, gegen Produktion geprüft; oder ein CDN liefert die Seite unter abweichendem Hostnamen aus. Der Fix bleibt derselbe: Die pageurl muss genau die Domain sein, an die das Token geht.

Fehler 2: Das Widget lädt gar nicht erst

ERROR: Invalid domain for site key

Diese Meldung erscheint, bevor eine Lösung angefordert wird, und ist immer konfigurationsseitig: Der Sitekey gibt die Seite nicht frei, das Widget läuft über localhost oder file://, oder es wird eine IP-Adresse statt eines Domainnamens aufgerufen. Am fremden Sitekey lässt sich nichts ändern; in eigenen Testumgebungen tragen Sie die Domain im reCAPTCHA-Admin nach.

Fehler 3: Token korrekt gelöst, Server lehnt trotzdem ab

{
    "success": false,
    "error-codes": ["invalid-input-response"]
}

Der Klassiker in Pipelines: Die Lösung kommt sauber zurück, das Token gehört aber zu einer anderen Domain als der geprüften.

# WRONG: pageurl doesn't match actual target
submit = requests.post("https://ocr.captchaai.com/in.php", data={
    "key": API_KEY,
    "method": "userrecaptcha",
    "googlekey": sitekey,
    "pageurl": "https://example.com/login",  # ← Must match actual domain
    "json": 1,
})

# But submitting token to:
requests.post("https://app.example.com/login", ...)  # Different subdomain!

Ein Blick auf beide Stellen – Solver-Aufruf und Ziel-Request – deckt den Unterschied sofort auf. Definieren Sie die Ziel-URL an einer Stelle und reichen Sie sie an beide Aufrufe weiter.


Schnelldiagnose: Symptom, Ursache, Fix

Symptom Wahrscheinliche Ursache Diagnose Fix
Token wird immer abgelehnt Page-URL passt nicht zur Zieldomain Solver-URL mit Ziel-URL vergleichen Page-URL angleichen
Läuft auf www, scheitert ohne www Abweichende Domain-Variante Weiterleitung prüfen Die Variante der Zielseite nutzen
Mal erfolgreich, mal nicht CDN liefert wechselnde Hostnamen Domain pro Anfrage vergleichen Feste URL aus der Kette
Im Browser erfolgreich, im Skript nicht Anderer Ursprung im Skript Adressleiste mit Skript-URL vergleichen Browser-URL übernehmen
Enterprise-Token abgelehnt Falsche Projekt- oder Domain-Bindung Sitekey-Zuordnung prüfen Domains in der Enterprise Console pflegen

Hinweis: Prüfen Sie zuerst, ob der Fehler bei jedem Lauf auftritt. Deterministisch heißt Domain-Fehler; sporadisch deutet auf abgelaufene Token oder Rate-Limiting hin.


Praxisbeispiel: Nightly-Pipeline gegen die eigene Staging-Umgebung

Ein Team in Köln testet den Login seines eigenen Shopware-Backends automatisiert. Die Staging-Instanz läuft auf einem Hetzner-Server unter staging.shop.example; die GitLab-CI-Pipeline zieht die Ziel-URL aber aus einer Variable, die noch auf shop.example zeigt und dort per Weiterleitung landet. Lokal läuft der Test durch, nachts scheitert er mit invalid-input-response – ein Domain-Fehler, kein Solver-Problem. Mit der finalen URL in der Variable ist die Fehlerklasse erledigt.

Danach hängt der Durchsatz nur an der Parallelität: CaptchaAI rechnet pro gleichzeitigem Thread ab, nicht pro Lösung – für eine Nightly-Pipeline mit wenigen parallelen Jobs reicht BASIC (15 $/Monat, 5 Threads). Preise in US-Dollar.


Wie streng prüft reCAPTCHA Subdomains?

Standardmäßig ist die Prüfung kein strikter Subdomain-Abgleich. Was zählt, ist die Registrierung des Betreibers:

Registrierte Domain Akzeptierte Herkunft
example.com example.com, www.example.com, sub.example.com (bei aktivierter Wildcard)
www.example.com nur www.example.com (bei strikter Konfiguration)
*.example.com jede Subdomain von example.com
localhost nur localhost (für die Entwicklung)

Permissive oder strikte Prüfung auf Serverseite

Ob eine Subdomain durchgeht, entscheidet die Gegenseite – beide Varianten sind üblich:

# Permissive validation (accepts any subdomain)
def validate_permissive(token, secret, base_domain):
    result = requests.post(
        "https://www.google.com/recaptcha/api/siteverify",
        data={"secret": secret, "response": token},
    ).json()

    if not result.get("success"):
        return False

    hostname = result.get("hostname", "")
    return hostname == base_domain or hostname.endswith(f".{base_domain}")


# Strict validation (exact match only)
def validate_strict(token, secret, expected_hostname):
    result = requests.post(
        "https://www.google.com/recaptcha/api/siteverify",
        data={"secret": secret, "response": token},
    ).json()

    return result.get("success") and result.get("hostname") == expected_hostname

Vier Fixes gegen Domain-Fehler in der Automatisierung

Fix 1: Page-URL exakt angleichen

Eine Variable für die Ziel-URL, die sowohl in die Solver-Anfrage als auch in den späteren Request geht:

# Correct: pageurl matches where you'll submit the token
target_url = "https://www.example.com/login"

submit = requests.post("https://ocr.captchaai.com/in.php", data={
    "key": API_KEY,
    "method": "userrecaptcha",
    "googlekey": "6LcR_RsTAAAAAN_r0GEkGBfq3L7KmU5JbPHJtwNp",
    "pageurl": target_url,  # Must match the actual domain
    "json": 1,
})

Fix 2: www und Nicht-www sauber behandeln

Prüfen Sie, welche Variante die Zielseite tatsächlich ausliefert:

from urllib.parse import urlparse

def normalize_url(url):
    """Normalize URL for consistent domain matching."""
    parsed = urlparse(url)
    # Use exactly what the target site uses
    # Check if the site redirects www → non-www or vice versa
    return f"{parsed.scheme}://{parsed.netloc}{parsed.path}"

# Test which variant the site uses
response = requests.get("https://example.com/login", allow_redirects=True)
actual_url = response.url  # May be https://www.example.com/login after redirect

Fix 3: Der Weiterleitungskette bis zum Ziel folgen

Manche Portale reichen den Login an einen separaten Auth-Host weiter:

def get_final_url(url):
    """Follow redirects to find the actual CAPTCHA page domain."""
    response = requests.get(url, allow_redirects=True, timeout=15)
    return response.url

# Login URL might redirect:
# https://example.com/login → https://auth.example.com/login
final_url = get_final_url("https://example.com/login")
# Use final_url as pageurl for solver

Das Ergebnis gehört in die Konfiguration, nicht in jeden einzelnen Lauf.

Fix 4: Domain aus dem reCAPTCHA-iframe ableiten

Bei unübersichtlicher Seitenstruktur hilft ein Blick auf das eingebettete Widget:

from bs4 import BeautifulSoup
from urllib.parse import urlparse

def extract_recaptcha_domain(html, page_url):
    """Extract the domain reCAPTCHA uses for token binding."""
    soup = BeautifulSoup(html, "html.parser")

    # Check for reCAPTCHA iframe
    iframe = soup.find("iframe", src=lambda s: s and "recaptcha" in s)
    if iframe:
        src = iframe.get("src", "")
        # The iframe URL may contain the domain parameter
        if "domain=" in src:
            # Extract domain from iframe URL
            pass

    # Default: use the page URL's domain
    return urlparse(page_url).netloc

Diagnose-Skript vor dem ersten Solve

Klären Sie die Domain-Situation vorab, statt Fehler nach dem Absenden zu analysieren. Das Skript folgt Weiterleitungen, vergleicht die www-Varianten und nennt die passende Page-URL:

import requests
from urllib.parse import urlparse

class DomainDiagnostic:
    """Diagnose domain verification issues for reCAPTCHA solving."""

    def __init__(self, target_url):
        self.target_url = target_url
        self.issues = []

    def check_redirects(self):
        """Check if the URL redirects to a different domain."""
        try:
            response = requests.get(
                self.target_url, allow_redirects=True, timeout=15,
                headers={"User-Agent": "Mozilla/5.0 Chrome/120.0.0.0"},
            )
            final_url = response.url
            original_domain = urlparse(self.target_url).netloc
            final_domain = urlparse(final_url).netloc

            if original_domain != final_domain:
                self.issues.append({
                    "type": "redirect",
                    "message": f"Redirects from {original_domain} to {final_domain}",
                    "fix": f"Use pageurl: {final_url}",
                })

            return final_url
        except Exception as e:
            self.issues.append({"type": "error", "message": str(e)})
            return self.target_url

    def check_www_variant(self):
        """Check if www and non-www point to the same content."""
        parsed = urlparse(self.target_url)
        domain = parsed.netloc

        if domain.startswith("www."):
            alt_domain = domain[4:]
        else:
            alt_domain = f"www.{domain}"

        alt_url = self.target_url.replace(domain, alt_domain)

        try:
            alt_response = requests.get(alt_url, allow_redirects=True, timeout=10)
            alt_final = urlparse(alt_response.url).netloc

            if alt_final != domain and alt_final != alt_domain:
                self.issues.append({
                    "type": "www_redirect",
                    "message": f"{alt_domain} redirects to {alt_final}",
                })
        except Exception:
            pass

    def report(self):
        """Generate diagnostic report."""
        final_url = self.check_redirects()
        self.check_www_variant()

        print(f"Target URL: {self.target_url}")
        print(f"Final URL:  {final_url}")
        print(f"Use as pageurl: {final_url}")

        if self.issues:
            print("\nIssues found:")
            for issue in self.issues:
                print(f"  [{issue['type']}] {issue['message']}")
                if "fix" in issue:
                    print(f"  Fix: {issue['fix']}")
        else:
            print("\nNo domain issues detected.")


# Usage
diag = DomainDiagnostic("https://example.com/login")
diag.report()
Ausgabe Bedeutung
Use as pageurl Dieser Wert gehört in die Solver-Anfrage
redirect Ihre URL ist nicht die Seite, auf der das Widget rendert
www_redirect Die Domain-Variante löst je nach Umgebung anders auf

Häufige Fragen

Liegt es an der Domain oder an einem abgelaufenen Token?

An der Domain, wenn der Fehler bei jedem Lauf auftritt. reCAPTCHA-Tokens sind nur rund zwei Minuten gültig; Ablauffehler treten sporadisch auf und hängen an langen Wartezeiten zwischen Lösung und Absenden.

Welche URL gehört in die pageurl, wenn die Seite weiterleitet?

Die letzte URL der Kette – die Seite, auf der das Widget rendert. Ermitteln Sie sie einmalig mit aktivierten Weiterleitungen und hinterlegen Sie den Wert in der Konfiguration.

Brauche ich für www.example.com und example.com getrennte Sitekeys?

Nein, ein Sitekey kann mehrere Hostnamen abdecken. Entscheidend ist, welche davon freigegeben sind und wie streng der Server den zurückgegebenen hostname auswertet.

Verändern Proxys oder ein CDN den Hostnamen im Token?

Das Token selbst nicht. Ein Proxy kann aber dazu führen, dass Ihr Skript eine andere Variante der Seite erreicht als Ihr Browser – etwa einen regionalen CDN-Hostnamen. Prüfen Sie die finale URL aus Sicht des Skripts.

Gilt dieselbe Domain-Bindung auch für Cloudflare Turnstile?

Ja. Auch Turnstile bindet seine Antwort an die Seite, auf der die Abfrage lief; für GeeTest v3 gilt dasselbe. Nur die Feldnamen im Formular unterscheiden sich – bei reCAPTCHA ist es g-recaptcha-response.


Fazit

Der häufigste Fehler in Automatisierungen ist keine misslungene Lösung, sondern eine unpassende Page-URL: Die URL, die Sie an CaptchaAI übergeben, muss der Domain entsprechen, an die das Token geht. Folgen Sie den Weiterleitungen bis zur echten Zielseite, klären Sie die www-Variante einmal verbindlich und lassen Sie vorher das Diagnose-Skript laufen.

Verwandte Leitfäden

Kommentare sind für diesen Artikel deaktiviert.