Token đúng, textarea g-recaptcha-response đã có giá trị, nhưng nút gửi vẫn xám — đây là lỗi phổ biến nhất khi tự động hóa reCAPTCHA v2. Nguyên nhân gần như luôn là: bạn mới điền token mà chưa gọi callback, hàm JavaScript do trang web đăng ký. Khi người dùng thật giải xong, script của Google gọi hàm đó, và chính nó mới mở khóa nút gửi hoặc bắn request AJAX. Chèn token là điều kiện cần, gọi callback là điều kiện đủ.
Nhận diện nhanh: triệu chứng nào ứng với nguyên nhân nào
| Vấn đề | Nguyên nhân | Cách xử lý |
|---|---|---|
| Form vẫn bị khóa sau khi chèn token | Chưa gọi callback | Đọc data-callback và gọi hàm đó |
ReferenceError: function not defined |
Callback nằm trong closure, không gắn vào window |
Dùng nhánh dự phòng ___grecaptcha_cfg |
| Token đã chèn nhưng không có request AJAX nào | Callback bắn AJAX chứ không submit form | Đọc nội dung hàm callback để biết nó làm gì |
| Server trả lỗi dù token được chấp nhận ở client | Token hết hạn trước khi submit | Giải sát thời điểm submit |
| Trang có nhiều widget, chỉ một cái hoạt động | Kịch bản lấy nhầm widget đầu tiên | Duyệt từng .g-recaptcha và xử lý theo widgetId |
Callback được đăng ký như thế nào
Trang web khai báo callback ngay trên widget reCAPTCHA:
<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>
Khi thử thách được giải, JavaScript của Google gọi onCaptchaSuccess(token) với một đối số duy nhất: chuỗi token. Toàn bộ logic mở khóa form nằm bên trong hàm này chứ không ở giá trị textarea. Bạn sẽ gặp ba dạng đăng ký:
data-callbacktrên thẻ widget — phổ biến nhất, tên hàm nằm sẵn trong HTML.- Tùy chọn
callbacktruyền vàogrecaptcha.render()— hàm có thể ẩn danh, không gắn vàowindow. - Không đăng ký callback nào — form tự đọc
grecaptcha.getResponse()ở bước submit.
Ba cách tìm đúng tên hàm callback
Đừng hardcode tên hàm: nhiều site sinh nó động hoặc giấu trong closure sau khi bundler đóng gói.
Cách 1: đọc thuộc tính data-callback
// In browser console
const widget = document.querySelector('.g-recaptcha');
const callbackName = widget?.getAttribute('data-callback');
console.log('Callback:', callbackName);
Cách 2: soi phần khởi tạo grecaptcha.render
Một số site truyền callback qua tùy chọn của grecaptcha.render():
// 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]);
}
});
Cách 3: chặn ngay lúc widget đăng ký
Dùng khi hai cách trên trả về rỗng. Chạy trong DevTools trước khi trang tải (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);
};
Kích hoạt callback sau khi chèn token
Python (Selenium)
Luồng đầy đủ: lấy sitekey và tên callback từ DOM, gửi task tới in.php, polling res.php, ghi token vào textarea, rồi gọi hàm.
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")
Nhánh else mới đáng chú ý: không có data-callback thì script đọc ___grecaptcha_cfg — cấu trúc nội bộ reCAPTCHA giữ cho từng widget — rồi gọi hàm lấy được.
JavaScript (Puppeteer)
Cùng logic, gói trong một page.evaluate():
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);
Khi trang không khai báo data-callback
Không phải site nào cũng dùng callback. Nhiều form chỉ gọi grecaptcha.getResponse() lúc submit; cách gọn nhất là ghi đè hàm đó:
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()
Expired callback và bẫy token hết hạn
Nhiều site khai báo thêm data-expired-callback để khóa lại nút gửi khi token hết hiệu lực. Giải sớm rồi chờ vài phút mới submit thì hàm này có thể chạy và khóa form ngay sau khi bạn vừa mở được nó — trong log nhìn hệt lỗi "callback không chạy". Xử lý: đẩy bước giải xuống sát lúc submit.
// Check for expired callback
const expiredCallback = document.querySelector('.g-recaptcha')
?.getAttribute('data-expired-callback');
console.log('Expired callback:', expiredCallback);
Ví dụ thực tế: suite QA của một team outsourcing tại TP.HCM
Một team QA làm dự án web cho khách nước ngoài chạy khoảng 300 lượt regression test đăng nhập mỗi đêm trên staging có bật reCAPTCHA v2. Bản Selenium đầu tiên chèn token rồi gọi submit(), và khoảng một phần ba số lần chạy thất bại — không vì token sai, mà vì form dùng AJAX kích hoạt từ callback nên submit() không gửi gì cả. Đọc data-callback rồi gọi window[callback](token) là hết cả lớp lỗi này.
Về chi phí, 300 lượt mỗi đêm là khối lượng nhỏ với mô hình giá theo thread của CaptchaAI: gói BASIC ($15/tháng, 5 thread) đã đủ, vì thread là số lượt giải chạy song song (luồng giải đồng thời) chứ không phải hạn mức số lần giải. Chạy nhiều môi trường cùng lúc mới cần STANDARD ($30/tháng, 15 thread).
Checklist trước khi đưa kịch bản lên CI
- Đọc
data-callbacktừ DOM ở runtime, không hardcode tên hàm. - Ghi token vào
g-recaptcha-responsetrước, gọi callback sau. - Chuẩn bị nhánh dự phòng
___grecaptcha_cfgcho site giấu callback trong closure. - Kiểm tra
data-expired-callbackvà rút ngắn khoảng cách giải–submit. - Nếu trang có nhiều widget, xử lý theo
widgetIdthay vì luôn lấy phần tử đầu tiên.
Câu hỏi thường gặp
Vì sao nút gửi vẫn xám dù textarea đã có token?
Vì nút được mở khóa bên trong hàm callback, không phải bởi giá trị textarea. Hai bước cần làm sau khi chèn token:
- Gọi
window[<tên_callback>](token)nếu tên hàm đọc được từdata-callback. - Nếu không, dùng nhánh
___grecaptcha_cfgđể lấy hàm mà widget đã đăng ký.
Làm sao biết trang dùng callback hay dùng getResponse?
Kiểm tra data-callback trên .g-recaptcha trước. Nếu rỗng, chặn grecaptcha.render như ở Cách 3; vẫn rỗng thì gần như chắc chắn form đọc grecaptcha.getResponse() lúc submit — ghi đè hàm đó.
Một trang có nhiều reCAPTCHA thì callback xử lý ra sao?
Mỗi widget có widgetId và callback riêng, đừng giả định chỉ có index 0. Trình tự đúng:
- Duyệt tất cả phần tử
.g-recaptchatrên trang. - Đọc
data-callbackcủa từng widget. - Chèn token vào đúng textarea rồi gọi hàm tương ứng cho widget đó.
Token do CaptchaAI trả về có tự kích hoạt callback không?
Không. API chỉ trả chuỗi token qua res.php; đặt token vào g-recaptcha-response và gọi callback là phần bạn viết trong trình duyệt tự động.
reCAPTCHA v2 và v3 khác nhau thế nào ở khâu này?
| Khía cạnh | reCAPTCHA v2 | reCAPTCHA v3 |
|---|---|---|
| Widget hiển thị | Có | Không |
Thuộc tính data-callback |
Có | Không |
| Cách nhận token | Callback được gọi | grecaptcha.execute() trả Promise |
| Việc bạn phải làm sau khi có token | Chèn rồi gọi callback | Gửi kèm request, không cần gọi hàm nào |
Bước tiếp theo và bài đọc thêm
Lấy API key tại captchaai.com, chạy đoạn Selenium ở trên và xem nút gửi có mở khóa ngay lần đầu không. Ba bài dưới đi sâu vào các tình huống liền kề:
- reCAPTCHA v2 Invisible: nhận diện thời điểm kích hoạt — widget không hiển thị thì bắt trigger ở đâu.
- Trích xuất tham số reCAPTCHA từ mã nguồn trang — lấy
sitekeykhi nó không nằm trên thẻ widget. - Xử lý nhiều CAPTCHA trên một trang — nhiều widget, nhiều callback trong cùng một form.