Explainers

Механизм обратного вызова reCAPTCHA v2: как работают обратные вызовы и как их активировать

Токен получен, скрытая textarea заполнена, а кнопка «Отправить» по-прежнему неактивна — это самый частый тупик при автоматизации форм с reCAPTCHA v2. Причина почти всегда одна: страница ждёт не сам токен, а выполнение своей JavaScript-функции обратного вызова (callback), которую в обычном сценарии запускает виджет Google. Пока эта функция не вызвана, состояние формы не меняется, и клиентская валидация считает проверку непройденной.

Ниже — где искать имя callback, как вызвать его после вставки решённого токена из CaptchaAI, что делать, если у виджета callback вообще нет, и почему форма иногда блокируется повторно через пару минут после успешного решения.


Что такое callback у виджета reCAPTCHA v2

Сайт объявляет callback прямо в разметке виджета:

<div class="g-recaptcha"
     data-sitekey="6Le-SITEKEY"
     data-callback="onCaptchaSuccess"
     data-expired-callback="onCaptchaExpired">
</div>

<script>
function onCaptchaSuccess(token) {
  document.getElementById('submit-btn').disabled = false;
  document.getElementById('captcha-token').value = token;
}
</script>

Когда проверка пройдена, скрипт Google вызывает onCaptchaSuccess(token) и передаёт строку токена единственным аргументом. Дальше всё зависит от кода сайта — callback может:

  • снять блокировку с кнопки отправки;
  • прогнать клиентскую валидацию и только после неё разрешить сабмит;
  • сразу отправить AJAX-запрос, минуя обычную отправку формы.

Поэтому шаг «вставили токен — вызвали callback» пропускать нельзя: запись значения в textarea[name="g-recaptcha-response"] не порождает событий DOM, на которые могла бы отреагировать страница.


Три способа найти имя callback-функции

Имя функции не всегда лежит на поверхности. Проверяйте варианты по порядку — от самого дешёвого к самому трудоёмкому.

Способ 1: атрибут data-callback

// In browser console
const widget = document.querySelector('.g-recaptcha');
const callbackName = widget?.getAttribute('data-callback');
console.log('Callback:', callbackName);

Если атрибут есть, задача решена: функция объявлена в глобальной области и доступна как window[callbackName].

Способ 2: вызов grecaptcha.render() в исходном коде

Часть сайтов рендерит виджет из скрипта и передаёт callback параметром, а не атрибутом:

// Search page source for grecaptcha.render
document.querySelectorAll('script:not([src])').forEach(s => {
  if (s.textContent.includes('grecaptcha.render')) {
    console.log(s.textContent.match(/callback\s*:\s*(\w+)/)?.[1]);
  }
});

Способ 3: перехват регистрации виджета

Самый надёжный вариант для минифицированных сборок — подменить grecaptcha.render до инициализации виджета и посмотреть, что реально передаётся. Запустите фрагмент в DevTools до загрузки страницы (Sources → Snippets):

const origRender = grecaptcha.render;
grecaptcha.render = function(container, params) {
  console.log('Render callback:', params.callback);
  console.log('Expired callback:', params['expired-callback']);
  return origRender.apply(this, arguments);
};

Имя callback стоит извлекать динамически при каждом прогоне: сборщики фронтенда меняют идентификаторы между релизами, и жёстко прописанное имя ломает тест на следующем же деплое.


Вставка токена и вызов callback

Порядок одинаков в любом стеке:

  1. Прочитать sitekey и имя callback из DOM.
  2. Отправить задачу в CaptchaAI и дождаться готового токена.
  3. Записать токен в textarea[name="g-recaptcha-response"].
  4. Вызвать callback-функцию с этим токеном.
  5. Отправить форму — или дать это сделать самому callback.

Python и Selenium

import requests
import time
from selenium import webdriver
from selenium.webdriver.common.by import By

API_KEY = "YOUR_API_KEY"
driver = webdriver.Chrome()
driver.get("https://staging.example.com/qa-login")

# Extract sitekey and callback
sitekey = driver.find_element(
    By.CSS_SELECTOR, ".g-recaptcha"
).get_attribute("data-sitekey")

callback = driver.find_element(
    By.CSS_SELECTOR, ".g-recaptcha"
).get_attribute("data-callback")

# Solve with CaptchaAI
resp = requests.post("https://ocr.captchaai.com/in.php", data={
    "key": API_KEY,
    "method": "userrecaptcha",
    "googlekey": sitekey,
    "pageurl": driver.current_url,
    "json": "1",
}).json()
task_id = resp["request"]

token = None
for _ in range(24):
    time.sleep(5)
    result = requests.get("https://ocr.captchaai.com/res.php", params={
        "key": API_KEY, "action": "get", "id": task_id, "json": "1"
    }).json()
    if result["status"] == 1:
        token = result["request"]
        break

# Inject token into textarea
driver.execute_script("""
    document.querySelector('textarea[name="g-recaptcha-response"]').value = arguments[0];
""", token)

# Trigger the callback
if callback:
    driver.execute_script(f"window['{callback}'](arguments[0]);", token)
    print(f"Triggered callback: {callback}")
else:
    # Fallback: try ___grecaptcha_cfg
    driver.execute_script("""
        try {
            var widgetId = Object.keys(___grecaptcha_cfg.clients)[0];
            var callback = ___grecaptcha_cfg.clients[widgetId].aa.l.callback;
            if (typeof callback === 'function') callback(arguments[0]);
        } catch(e) {}
    """, token)
    print("Triggered callback via ___grecaptcha_cfg")

Опрос res.php идёт с интервалом 5 с и ограничен 24 попытками: после этого потолка задачу лучше признать неуспешной и повторить, чем держать поток занятым. Ветка с ___grecaptcha_cfg нужна для случаев, когда функция объявлена внутри замыкания и в window её нет: структура этого объекта недокументирована и может меняться, поэтому доступ к ней всегда оборачивайте в try/catch.

Node.js и Puppeteer

const puppeteer = require('puppeteer');

// After solving and getting the token...
await page.evaluate((token, callbackName) => {
  // Set textarea value
  const textarea = document.querySelector(
    'textarea[name="g-recaptcha-response"]'
  );
  textarea.value = token;
  textarea.style.display = 'block'; // sometimes hidden

  // Trigger callback
  if (callbackName && typeof window[callbackName] === 'function') {
    window[callbackName](token);
    console.log(`Called ${callbackName}()`);
  } else {
    // Fallback: search grecaptcha config
    try {
      const clients = ___grecaptcha_cfg.clients;
      const widgetId = Object.keys(clients)[0];
      const cb = clients[widgetId]?.aa?.l?.callback;
      if (typeof cb === 'function') cb(token);
    } catch (e) {}
  }
}, token, callbackName);

Обратите внимание на строку с textarea.style.display: элемент по умолчанию скрыт, и часть клиентских валидаторов игнорирует значения невидимых полей. В headless-режиме логика та же — отдельной обработки она не требует.


Виджеты без data-callback: переопределение getResponse()

Некоторые формы вообще не объявляют callback, а при отправке спрашивают токен у самого API — через grecaptcha.getResponse(). Вызывать в этом случае нечего: достаточно вернуть нужное значение из функции.

driver.execute_script("""
    const token = arguments[0];
    document.querySelector('textarea[name="g-recaptcha-response"]').value = token;
    // Override getResponse to return the token
    if (typeof grecaptcha !== 'undefined') {
        grecaptcha.getResponse = function() { return token; };
    }
""", token)

# Then submit the form normally
driver.find_element(By.CSS_SELECTOR, "form").submit()

Заполнять textarea всё равно нужно — у двух шагов разные адресаты:

  • поле g-recaptcha-response уходит в теле POST-запроса, и его читает сервер;
  • переопределённый getResponse() виден только клиентскому коду страницы.

expired-callback: почему форма блокируется повторно

Токен reCAPTCHA v2 живёт около двух минут. Если сайт объявил data-expired-callback, по истечении срока сработает уже эта функция — она вернёт кнопку в неактивное состояние, даже если ваш скрипт до этого всё сделал правильно. Классический симптом: сценарий стабильно проходит локально и падает в CI, где между решением и отправкой вклинивается медленный шаг.

// Check for expired callback
const expiredCallback = document.querySelector('.g-recaptcha')
  ?.getAttribute('data-expired-callback');
console.log('Expired callback:', expiredCallback);

Практическое правило: запрашивайте решение как можно ближе к моменту отправки. Заполните остальные поля, дождитесь готовности страницы и только потом отправляйте задачу в API.


Практический сценарий: ночной регресс формы входа

Типичная ситуация для распределённой команды: QA-инженеры в Минске и Алматы поддерживают несколько десятков end-to-end сценариев, которые ночью прогоняются на staging-стенде в европейском регионе. Форма входа закрыта reCAPTCHA v2, и до подключения решателя половина тестов просто не доходила до личного кабинета.

Что здесь важно с точки зрения callback:

  1. RTT съедает окно жизни токена. Если стенд и раннер разнесены по регионам, каждый лишний сетевой хоп сокращает запас времени до срабатывания expired-callback. Держите решение задачи последним шагом перед отправкой.
  2. Параллельность упирается в потоки, а не в число решений. Тарификация CaptchaAI построена на потоках: один поток — одна задача в работе, число решений внутри тарифа не ограничено. Для ночного прогона на 8–10 параллельных браузеров хватает BASIC ($15/мес, 5 потоков) или STANDARD ($30/мес, 15 потоков); для дневных прогонов на десятках агентов практичнее ADVANCE ($90/мес, 50 потоков). Фиксированная месячная цена в USD удобна командам, которые планируют бюджет на квартал вперёд.
  3. Логируйте найденное имя callback. Записывайте в отчёт, какое имя функции обнаружено и каким из трёх способов — при следующем релизе фронтенда это первое, что нужно сравнить.
  4. Собирайте только те данные, которые вправе обрабатывать. Если сценарии затрагивают формы с персональными данными, ограничивайтесь синтетическими учётными записями на staging — уместная предосторожность и для 152-ФЗ «О персональных данных», и для GDPR-контура. Это инженерная гигиена, а не юридическая консультация.

Диагностика: что проверять, когда форма молчит

Симптом Вероятная причина Что сделать
Форма остаётся заблокированной после вставки токена callback не был вызван Найдите имя функции и вызовите её явно
ReferenceError: function not defined callback объявлен внутри замыкания Используйте запасной путь через ___grecaptcha_cfg
Токен вставлен, но AJAX-запрос не уходит callback отправляет запрос сам, а не сабмитит форму Прочитайте тело функции и повторите её логику
Сервер отвечает ошибкой проверки срок действия токена истёк до отправки Решайте задачу непосредственно перед сабмитом
Значение поля игнорируется валидатором textarea скрыта Верните элементу видимость перед записью значения

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

Почему кнопка «Отправить» остаётся неактивной, хотя токен уже в textarea?

Кнопку разблокирует не токен, а код сайта внутри callback. Присвоение значения через JavaScript не порождает событий input/change, поэтому страница о нём не узнаёт. Вызовите функцию явно — через window['имя'](token) или через запасной путь ___grecaptcha_cfg.

Сколько живёт токен reCAPTCHA v2 и когда его лучше запрашивать?

Ориентировочно около двух минут. Отправляйте задачу в API последним шагом сценария, когда остальные поля формы уже заполнены, — так расходуется минимум окна и меньше риск нарваться на expired-callback.

Как вызвать callback, если он объявлен внутри замыкания?

Идите через внутренний конфиг: ___grecaptcha_cfg.clients → первый виджет → свойство callback. Структура объекта недокументирована, ключи меняются между версиями, поэтому обязательно оборачивайте вызов в try/catch и держите запасной сценарий с обычной отправкой формы.

Нужно ли вызывать callback в headless-режиме?

Да, логика полностью совпадает с обычным режимом браузера: JavaScript страницы выполняется так же. Дополнительно убедитесь, что скрытая textarea доступна для записи, а размер окна не ломает вёрстку формы.

Что делать, если на странице несколько виджетов reCAPTCHA v2?

Обрабатывайте каждый отдельно: у виджета свой sitekey, свой индекс в ___grecaptcha_cfg.clients и потенциально своё имя callback. Селектор document.querySelector('.g-recaptcha') вернёт только первый — используйте querySelectorAll и сопоставляйте виджеты с формами по DOM-дереву.


Решайте reCAPTCHA v2 с корректной обработкой callback

Получите API-ключ на captchaai.com и прогоните полный цикл — от чтения sitekey до вызова callback — на своём staging-стенде.


Похожие материалы

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