Test bằng requests hoặc cURL thì token CaptchaAI hợp lệ, nhưng cắm vào Selenium hay Playwright thì form vẫn báo lỗi — gần như luôn là một trong sáu nguyên nhân dưới đây, không phải lỗi CaptchaAI. API chỉ trả token; trình duyệt có chấp nhận hay không lại phụ thuộc JavaScript của trang.
Ví dụ thường gặp: team QA outsource ở TP.HCM test checkout trên staging bằng Playwright, nhận token hợp lệ từ CaptchaAI, nhưng nút "Đặt hàng" vẫn xám ngắt vì callback JavaScript chưa được gọi.
Kiểm tra nhanh:
- Token có set đúng phần tử site đang đọc, hay set nhầm phần tử ẩn?
- Callback của widget đã gọi thủ công chưa?
pageurlgửi cho CaptchaAI có đúng URL hiện tại, kể cả khi CAPTCHA nằm trong iframe?
Token kẹt ở đâu giữa API và trình duyệt
| Vấn đề | Triệu chứng | Nguyên nhân |
|---|---|---|
| Token sai phần tử | Form thiếu token | Sai bộ chọn textarea |
| Token hết hạn | CAPTCHA hiện lại | Giải và gửi cách nhau quá lâu |
| Callback chưa kích hoạt | Nút submit vẫn disable | Thiếu lệnh gọi grecaptcha |
pageurl sai |
Token không hợp lệ cho domain | URL không khớp thực tế |
| Lớp chống bot khác | Bị chặn dù token đã nhận | Fingerprint trình duyệt tự động hóa |
Cách 1: Set đúng token cho reCAPTCHA v2
Token phải nằm trong textarea g-recaptcha-response VÀ callback phải được gọi — thiếu một trong hai, form vẫn coi như chưa giải xong:
Nếu trang có nhiều widget reCAPTCHA,
querySelectorAllbên dưới set token cho tất cả — kiểm tra lại có đúng ý bạn không.
from selenium import webdriver
from selenium.webdriver.common.by import By
import time
def inject_recaptcha_token(driver, token):
"""Properly inject reCAPTCHA v2 token in browser."""
# Step 1: Make the response textarea visible and set the value
driver.execute_script("""
// Find all response textareas (may be multiple on page)
var textareas = document.querySelectorAll('[name="g-recaptcha-response"]');
textareas.forEach(function(ta) {
ta.style.display = 'block';
ta.value = arguments[0];
});
// Also set via ID if present
var byId = document.getElementById('g-recaptcha-response');
if (byId) {
byId.style.display = 'block';
byId.value = arguments[0];
}
""", token)
# Step 2: Trigger the callback
driver.execute_script("""
// Try standard callback
if (typeof ___grecaptcha_cfg !== 'undefined') {
var clients = ___grecaptcha_cfg.clients;
for (var key in clients) {
var client = clients[key];
// Navigate nested structure to find callback
for (var prop in client) {
var val = client[prop];
if (val && typeof val === 'object') {
for (var subprop in val) {
var subval = val[subprop];
if (subval && typeof subval === 'object' && subval.callback) {
subval.callback(arguments[0]);
return;
}
}
}
}
}
}
// Fallback: try common callback names
if (typeof onCaptchaSuccess === 'function') onCaptchaSuccess(arguments[0]);
else if (typeof captchaCallback === 'function') captchaCallback(arguments[0]);
""", token)
# Usage
driver = webdriver.Chrome()
driver.get("https://example.com/form")
# ... solve CAPTCHA via CaptchaAI ...
inject_recaptcha_token(driver, token)
time.sleep(1)
driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
Cách 2: Xử lý form chỉ mở khóa sau callback
Nhiều form giữ disabled đến khi callback chạy xong — set value textarea thôi chưa đủ:
def find_and_trigger_callback(driver, token):
"""Find the reCAPTCHA callback and trigger it."""
# Method 1: Extract callback from data attribute
callback_name = driver.execute_script("""
var widget = document.querySelector('.g-recaptcha');
if (widget) {
return widget.getAttribute('data-callback');
}
return null;
""")
if callback_name:
driver.execute_script(f"window['{callback_name}'](arguments[0]);", token)
return True
# Method 2: Extract from grecaptcha config
triggered = driver.execute_script("""
try {
var widgetId = 0;
var callback = ___grecaptcha_cfg.clients[widgetId].aa.l.callback;
if (typeof callback === 'function') {
callback(arguments[0]);
return true;
}
} catch(e) {}
return false;
""", token)
return triggered
Cách 3: Canh thời gian — giải xong dùng ngay
Token reCAPTCHA v2 có TTL ngắn. Giải xong mới đi log hay chuyển tab trước khi submit — token gần như chắc chắn hết hạn:
import requests
import time
def solve_and_inject_fast(driver, api_key, sitekey, page_url):
"""Solve CAPTCHA and inject token with minimal delay."""
# Submit solve request
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": api_key,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url,
"json": 1,
}, timeout=30)
task_id = resp.json()["request"]
# Poll for result
time.sleep(15)
for _ in range(24):
resp = requests.get("https://ocr.captchaai.com/res.php", params={
"key": api_key, "action": "get",
"id": task_id, "json": 1,
}, timeout=15)
data = resp.json()
if data.get("status") == 1:
token = data["request"]
# IMMEDIATELY inject — don't wait
inject_recaptcha_token(driver, token)
# Submit form within 5 seconds
time.sleep(0.5)
return True
if data["request"] != "CAPCHA_NOT_READY":
raise RuntimeError(data["request"])
time.sleep(5)
raise TimeoutError("Solve timeout")
Cách 4: Lấy đúng pageurl cho SPA và iframe
CAPTCHA trong iframe không đổi URL tab. Lấy pageurl từ biến lưu sẵn thay vì URL hiện tại thường khiến token bị từ chối vì domain không khớp:
driver.current_urlcó thể đổi ngay sau khi router chuyển trang — lấypageurlcàng sát thời điểm gọi API càng an toàn.
def get_correct_pageurl(driver):
"""Get the URL that reCAPTCHA actually sees."""
# Check if CAPTCHA is in an iframe
iframes = driver.find_elements(By.TAG_NAME, "iframe")
for iframe in iframes:
src = iframe.get_attribute("src") or ""
if "recaptcha" in src or "anchor" in src:
# CAPTCHA is in iframe — use parent page URL
return driver.current_url
# For SPAs, use current URL (may differ from initial load)
return driver.current_url
# WRONG — using the URL you navigated to
pageurl = "https://example.com/form" # May have redirected
# CORRECT — using the URL the browser is actually on
pageurl = get_correct_pageurl(driver)
Cách 5: Trang có lớp chống bot riêng, tách khỏi CAPTCHA
Token hợp lệ không đồng nghĩa trình duyệt được chấp nhận. Site có thể chạy thêm lớp kiểm tra hành vi song song — token qua được nhưng request sau vẫn nhận 403:
Cấu hình trình duyệt kiểm thử chuẩn để giảm tín hiệu tự động hóa trong QA nội bộ.
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
def create_stealth_driver():
"""Create a browser that avoids basic bot detection."""
options = Options()
# Remove automation indicators
options.add_argument("")
options.add_experimental_option("excludeSwitches", ["enable-automation"])
options.add_experimental_option("useAutomationExtension", False)
# Set realistic window size
options.add_argument("--window-size=1920,1080")
driver = webdriver.Chrome(options=options)
# Override navigator.webdriver
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
"source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
})
return driver
driver = create_stealth_driver()
Cách 6: Set token cho Cloudflare Turnstile
Turnstile không dùng chung selector với reCAPTCHA v2 — copy nguyên code Cách 1 là lỗi hay gặp nhất:
def inject_turnstile_token(driver, token):
"""Inject Turnstile token into the page."""
driver.execute_script("""
// Find Turnstile response input
var inputs = document.querySelectorAll(
'input[name="cf-turnstile-response"], ' +
'input[name="g-recaptcha-response"]'
);
inputs.forEach(function(input) {
input.value = arguments[0];
});
// Trigger change event
inputs.forEach(function(input) {
input.dispatchEvent(new Event('change', { bubbles: true }));
input.dispatchEvent(new Event('input', { bubbles: true }));
});
// Try Turnstile callback
if (window.turnstile) {
var widgets = document.querySelectorAll('[data-callback]');
widgets.forEach(function(w) {
var cb = w.getAttribute('data-callback');
if (typeof window[cb] === 'function') {
window[cb](arguments[0]);
}
});
}
""", token)
Script kiểm tra nhanh khi vẫn bí
Chạy ngay sau khi set token để biết chính xác bước nào thiếu:
def debug_captcha_injection(driver, token):
"""Print debug info to diagnose injection failures."""
info = driver.execute_script("""
var result = {};
// Check textarea exists
var ta = document.querySelector('[name="g-recaptcha-response"]');
result.textarea_found = !!ta;
result.textarea_value_set = ta ? ta.value.length > 0 : false;
// Check reCAPTCHA loaded
result.grecaptcha_loaded = typeof grecaptcha !== 'undefined';
// Check for callback
var widget = document.querySelector('.g-recaptcha');
result.has_data_callback = widget ? !!widget.getAttribute('data-callback') : false;
result.callback_name = widget ? widget.getAttribute('data-callback') : null;
// Current URL
result.current_url = window.location.href;
// Check for errors in console (basic)
result.has_submit_button = !!document.querySelector('button[type=submit], input[type=submit]');
return result;
""")
for key, value in info.items():
print(f" {key}: {value}")
return info
Tra nhanh theo triệu chứng
| Triệu chứng | Nguyên nhân | Cách sửa |
|---|---|---|
| Set token nhưng form từ chối | Callback chưa kích hoạt | Tìm và gọi đúng hàm callback |
| Nút submit vẫn disable | Callback mở khóa nút | Kích hoạt callback sau khi set token |
| "CAPTCHA không hợp lệ" sau submit | Token hết hạn | Rút ngắn thời gian giải và gửi |
| 403 sau submit | Lớp chống bot khác, không phải CAPTCHA | Kiểm tra cấu hình trình duyệt kiểm thử |
Ổn với requests, lỗi trong trình duyệt |
pageurl khác nhau |
Dùng driver.current_url làm pageurl |
Câu hỏi thường gặp
Vì sao token qua API thành công nhưng trình duyệt vẫn từ chối?
Thường do callback của widget chưa kích hoạt — set textarea thôi chưa đủ để bật nút submit.
Playwright và Puppeteer có lỗi set token giống Selenium không?
Có, vì nguyên nhân nằm ở JavaScript của widget — đổi execute_script thành page.evaluate(), logic callback giữ nguyên.
Làm sao biết token đã hết hạn trước khi submit?
Đo thời gian từ lúc res.php trả status: 1 đến lúc submit; quá vài chục giây thì giải lại token.
Headless có làm tăng khả năng bị từ chối token không?
Có, với site kiểm tra hành vi mạnh. Thử headful trước khi chuyển sang headless.
Retry nhiều lần khi debug có tốn thêm chi phí không?
Không tính thêm theo lần — CaptchaAI tính theo thread, giải không giới hạn trong tháng. Nhưng mỗi retry vẫn chiếm một thread, gói nhỏ như BASIC ($15/tháng, 5 thread) debug dồn dập dễ nghẽn job khác.