Токен получен, скрытая 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
Порядок одинаков в любом стеке:
- Прочитать
sitekeyи имя callback из DOM. - Отправить задачу в CaptchaAI и дождаться готового токена.
- Записать токен в
textarea[name="g-recaptcha-response"]. - Вызвать callback-функцию с этим токеном.
- Отправить форму — или дать это сделать самому 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:
- RTT съедает окно жизни токена. Если стенд и раннер разнесены по регионам, каждый лишний сетевой хоп сокращает запас времени до срабатывания
expired-callback. Держите решение задачи последним шагом перед отправкой. - Параллельность упирается в потоки, а не в число решений. Тарификация CaptchaAI построена на потоках: один поток — одна задача в работе, число решений внутри тарифа не ограничено. Для ночного прогона на 8–10 параллельных браузеров хватает BASIC ($15/мес, 5 потоков) или STANDARD ($30/мес, 15 потоков); для дневных прогонов на десятках агентов практичнее ADVANCE ($90/мес, 50 потоков). Фиксированная месячная цена в USD удобна командам, которые планируют бюджет на квартал вперёд.
- Логируйте найденное имя callback. Записывайте в отчёт, какое имя функции обнаружено и каким из трёх способов — при следующем релизе фронтенда это первое, что нужно сравнить.
- Собирайте только те данные, которые вправе обрабатывать. Если сценарии затрагивают формы с персональными данными, ограничивайтесь синтетическими учётными записями на 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-стенде.