Troubleshooting

reCAPTCHA: ошибки и исправления проверки домена

Если решатель вернул корректный токен, а бэкенд отвечает invalid-input-response, дело почти всегда не в решении, а в домене: токен привязан к одному имени хоста, а проверяется на другом. Хватает расхождения в www. или в один поддомен после редиректа, и сбой молчаливый — в логах решателя ошибки нет. Разбор ниже устроен так:

  • где reCAPTCHA сверяет домен;
  • три сценария отказа;
  • правила сопоставления поддоменов;
  • как найти реальный pageurl.

Почему токен привязан к домену

reCAPTCHA вшивает имя хоста страницы в токен: владелец сайта регистрирует разрешённые домены для sitekey, а Google при валидации возвращает имя хоста, где токен был сгенерирован.

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 }

Три точки, где сверяется домен

Точка проверки Что именно сверяется
Клиентская часть Виджет грузится только на разрешённых доменах (можно отключить)
Генерация токена Вшитое в токен имя хоста совпадает с источником страницы
Валидация на сервере siteverify возвращает hostname, сервер решает, принять ли его

Вывод для автоматизации:

  • первую и третью строку контролирует владелец сайта;
  • вы контролируете только вторую — через pageurl;
  • имя хоста в токене должно совпасть с адресом отправки.

Три ошибки, которые дают почти все обращения в поддержку

Ошибка 1: в ответе siteverify не то имя хоста

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

Токен валиден, но в поле hostname не тот домен, которого ждёт сервер. Строгая валидация его отбраковывает:

# 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

Откуда берётся:

  • токен решён для www.example.com, а проверяется на example.com;
  • токен решён для staging.example.com, а проверяется на example.com;
  • прокси или CDN подменяет имя хоста.

Что делать: взять домен, на который реально уходит токен, и подставить его в pageurl запроса к решателю.

Ошибка 2: виджет отказывается загружаться

Виджет reCAPTCHA не отрисовывается либо показывает ошибку:

ERROR: Invalid domain for site key

Откуда берётся: домена страницы нет в списке разрешённых; виджет открыт с localhost или по file://; вместо домена используется IP-адрес.

Что делать: на чужом сайте — никак. На своём staging-стенде добавьте домен в консоли reCAPTCHA до тестов.

Ошибка 3: токен отклонён, хотя решён корректно

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

Токен создан для одного домена, а проверяется на другом. Частая причина — pageurl задачи не совпадает с целевым:

# 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://staging.example.com/qa-login",  # ← Must match actual domain
    "json": 1,
})

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

В последних строках задача уходит с одним хостом, а токен — на другой поддомен: для reCAPTCHA это разные домены.


Таблица быстрой диагностики

Симптом Вероятная причина Как проверить Что сделать
Токен отклоняется всегда pageurl не совпадает с доменом отправки Сравнить pageurl задачи с доменом отправки Привести pageurl в соответствие
На www работает, без www — нет Разошлись варианты домена Посмотреть редиректы Взять вариант, который использует сайт
Иногда проходит, иногда нет CDN отдаёт разные домены Проверить домен от запроса к запросу Зафиксировать один URL из цепочки
В браузере работает, в скрипте — нет Другой источник запроса Сверить адресную строку с pageurl Взять конечный URL из браузера
Токен Enterprise отклонён Не та привязка проекта Проверить привязку Enterprise sitekey Уточнить домены в консоли Enterprise

Правила сопоставления доменов

Все три ошибки упираются в один вопрос: насколько строго сайт сверяет поддомены. Решает это владелец сайта.

Точное совпадение и подстановочный знак

Зарегистрированный домен Допустимый источник
example.com example.com, www.example.com, sub.example.com (с подстановочным знаком)
www.example.com только www.example.com (строгий режим)
*.example.com любой поддомен example.com
localhost только localhost (разработка)

Как ведёт себя проверка hostname на сервере

Поле hostname отражает страницу, где токен был сгенерирован. Дальше решает валидация на бэкенде:

# 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

Мягкий вариант прощает www и любой поддомен, строгий требует посимвольного совпадения. Без доступа к коду валидации исходите из строгого сценария.


Как чинить доменные ошибки в автоматизации

Приёмы ниже — по возрастанию сложности.

Приём 1: сделать pageurl точным

Передавайте ровно тот адрес, куда уйдёт токен:

# Correct: pageurl matches where you'll submit the token
target_url = "https://www.https://staging.example.com/qa-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,
})

Приём 2: развести www и не-www

Не гадайте — спросите у сайта:

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://staging.example.com/qa-login", allow_redirects=True)
actual_url = response.url  # May be https://www.https://staging.example.com/qa-login after redirect

Приём 3: вытащить домен из цепочки редиректов

Частый случай — форма входа на auth-хосте:

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://staging.example.com/qa-login → https://auth.https://staging.example.com/qa-login
final_url = get_final_url("https://staging.example.com/qa-login")
# Use final_url as pageurl for solver

Приём 4: посмотреть, какой домен использует сам виджет

Нужен, когда виджет вынесен в iframe:

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

Скрипт диагностики домена

Класс ниже отвечает на два вопроса:

  • меняется ли домен после редиректов;
  • расходятся ли варианты с www. и без него.
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://staging.example.com/qa-login")
diag.report()

Запускайте его на каждую новую цель.


Локальный сценарий: один стенд, четыре домена

Знакомая командам из Москвы, Минска и Алматы схема: QA-стенд в европейском регионе облака, перед ним CDN. У приложения одновременно живут staging.example.com, вариант с www., auth-хост формы входа и превью-домен на каждую ветку. Разработчик берёт адрес из закладок — и получает отказ.

Что помогает:

  • прогонять URL через get_final_url() до отправки задачи;
  • на нестабильных мобильных сетях добавлять повторную попытку;
  • собирать со стенда только те данные, которые вы вправе обрабатывать (152-ФЗ для РФ, дисциплина в духе GDPR для кросс-граничных команд).

Часто задаваемые вопросы

Что означает ошибка «Invalid domain for site key»?

Домена страницы с виджетом нет в списке разрешённых для sitekey. Список редактирует владелец сайта.

Как понять, какой pageurl использовать?

Возьмите конечный адрес после редиректов: requests.get(url, allow_redirects=True).url. Передавайте именно его, а не набранный руками.

Что проверять первым делом, если токен отклонён?

Сравните hostname из ответа siteverify с адресом отправки токена — расхождение в www. или в поддомене видно сразу.

Почему на стенде токен проходит, а в продакшене — нет?

Четыре обычные причины:

  • на тестах localhost с особыми правилами домена;
  • в продакшене CDN отдаёт другой домен;
  • включена более строгая проверка hostname;
  • другая цепочка редиректов.

Где здесь CaptchaAI

Что сервис решает и чего не решает:

  • reCAPTCHA v2 и v3, включая Enterprise-варианты;
  • Cloudflare Turnstile и Challenge, GeeTest v3;
  • image/OCR-, grid- и BLS-капчи;
  • CaptchaFox (beta), Friendly Captcha (beta), Lemin (beta);
  • hCaptcha и FunCaptcha не решает, GeeTest v4 — «скоро».

Сервис привязывает токен к переданному pageurl, но конфигурацию доменов не проверяет.

Оплата идёт за одновременные потоки, а не за каждое решение: BASIC ($15/мес, 5 потоков), STANDARD ($30/мес, 15 потоков), ADVANCE ($90/мес, 50 потоков) и далее до VIP-3 ($7,500/мес, 5000 потоков). Итерации отладки pageurl счёт не увеличивают.


Коротко

Доменные ошибки reCAPTCHA — почти всегда рассинхрон между pageurl задачи и адресом отправки токена. Порядок работы:

  1. Пройти редиректы до конечного URL.
  2. Зафиксировать вариант с www или без него.
  3. Сверить hostname из ответа siteverify с ожидаемым.
  4. И только после этого отправлять задачу в CaptchaAI.

Похожие статьи

Комментарии для этой статьи отключены.