Mọi lỗi xác minh tên miền của reCAPTCHA đều quy về một câu: token sinh ra ở hostname này nhưng lại được xác thực ở hostname khác. Trong tự động hóa, thủ phạm gần như luôn là pageurl gửi cho solver: nó trỏ tới staging.example.com trong khi form thật nằm ở auth.staging.example.com.
Khó chịu ở chỗ lỗi này im lặng: token trông bình thường, log không có exception, chỉ có tỷ lệ submit tụt dần. Bên dưới là bảng triage, cơ chế gắn hostname vào token và bốn cách sửa pageurl.
Bảng triage nhanh khi token bị từ chối
Đối chiếu triệu chứng bạn đang gặp; phần lớn case dừng lại ở hàng đầu tiên.
| Triệu chứng | Nguyên nhân thường gặp | Cách chẩn đoán | Cách sửa |
|---|---|---|---|
| Token luôn bị từ chối | pageurl lệch tên miền đích |
So pageurl với domain thật sự nhận token |
Sửa pageurl cho khớp |
Chạy được trên www, hỏng trên non-www |
Lệch biến thể tên miền | Xem site redirect về phía nào | Dùng biến thể sau redirect |
| Lúc được lúc không | CDN hoặc load balancer trả domain khác nhau | Ghi log hostname từng lần chạy | Cố định URL cuối chuỗi redirect |
| Trình duyệt thì được, script thì trượt | Script gửi từ origin khác | So URL trên thanh địa chỉ với pageurl |
Lấy URL trình duyệt dừng lại |
| Token reCAPTCHA Enterprise bị từ chối | Sai project hoặc sai ràng buộc domain | Đối chiếu site key với domain đang chạy | Sửa danh sách domain trong console |
reCAPTCHA gắn tên miền vào token như thế nào
Luồng dưới tóm tắt cơ chế: hostname được nhúng vào token lúc widget sinh ra nó, rồi Google trả hostname đó về khi backend gọi siteverify.
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 }
Điểm cần nhớ: Google không tự từ chối token lệch hostname — nó chỉ trả hostname về, quyết định nằm ở backend trang đích.
Ba điểm tên miền bị kiểm tra
- Phía client — widget chỉ tải trên domain đã đăng ký; chủ site có thể tắt lớp này.
- Lúc sinh token — hostname nhúng vào token chính là origin của trang đang mở.
- Lúc backend xác thực —
siteverifytrả hostname về, backend tự quyết định nhận hay không.
Ba lỗi tên miền hay gặp nhất
Lỗi 1: hostname trong phản hồi siteverify không khớp
Lỗi này đến từ ba tình huống quen thuộc:
- Giải token cho
www.example.comnhưng gửi token lênexample.com - Giải token cho
staging.example.comnhưng xác thực ở môi trường khác - Proxy hoặc CDN trước ứng dụng làm đổi hostname nhìn thấy được
Dấu vết nằm ngay trong phản hồi siteverify:
{
"success": true,
"hostname": "subdomain.example.com",
"challenge_ts": "2025-01-15T10:30:00Z"
}
Token hợp lệ, success là true, nhưng hostname lại là domain khác. Nhiều backend so khớp cứng ở bước này:
# 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
Cách sửa: để pageurl gửi cho solver trùng khít domain nơi token sẽ được submit, kể cả phần www và subdomain.
Lỗi 2: widget báo "Invalid domain for site key"
Widget reCAPTCHA không render, hoặc render ra thông báo lỗi:
ERROR: Invalid domain for site key
| Tình huống | Vì sao bị chặn |
|---|---|
| Domain hiện tại không có trong site key | Key chỉ nhận domain đã đăng ký |
Trang chạy từ localhost hoặc file:// |
Không phải origin hợp lệ |
| Truy cập bằng địa chỉ IP | reCAPTCHA đối chiếu tên miền, không phải IP |
Trong tự động hóa: đây là lỗi cấu hình phía chủ site, không phải lỗi solver. Nếu bạn sở hữu hệ thống, thêm domain vào site key; nếu chỉ chạy test, truyền pageurl bằng domain đã được phép.
Lỗi 3: token bị từ chối dù đã giải thành công
{
"success": false,
"error-codes": ["invalid-input-response"]
}
invalid-input-response hay bị đọc nhầm thành "token hỏng". Token vẫn nguyên vẹn, chỉ là nó sinh cho một domain khác. Kịch bản kinh điển trong pipeline tự động: pageurl không trùng domain đích:
# 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.staging.example.com/qa-login", ...) # Different subdomain!
Quy tắc khớp tên miền: wildcard hay khớp chính xác
Mặc định reCAPTCHA không so khớp subdomain nghiêm ngặt; mức chặt tùy cách chủ site đăng ký domain.
| Domain đã đăng ký | Origin được chấp nhận |
|---|---|
example.com |
example.com, www.example.com, sub.example.com (nếu bật wildcard) |
www.example.com |
Chỉ www.example.com (nếu cấu hình chặt) |
*.example.com |
Mọi subdomain của example.com |
localhost |
Chỉ localhost (dùng khi phát triển) |
Máy chủ đích mới là bên quyết định mức chặt
Cùng một token có thể qua ở site này và trượt ở site kia, chỉ vì hai backend kiểm tra khác nhau:
# 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
Khi tự động hóa trên hệ thống của mình, chọn kiểu chặt và ghi log hostname — dữ liệu chẩn đoán rẻ nhất bạn có.
Bốn cách sửa pageurl trong pipeline tự động
Cách 1: cho pageurl khớp chính xác URL đích
Cách này xử lý phần lớn case: pageurl phải là URL của trang chứa widget, đúng scheme và host:
# Correct: pageurl matches where you'll submit the token
target_url = "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,
})
Cách 2: xử lý biến thể www và non-www
Đừng đoán biến thể nào đúng; hỏi chính trang đích bằng một request allow_redirects=True:
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://staging.example.com/qa-login after redirect
Cách 3: đi theo chuỗi redirect để lấy tên miền thật
Nhiều hệ thống tách trang đăng nhập sang subdomain riêng, nên URL trong config không phải URL cuối cùng:
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.staging.example.com/qa-login
final_url = get_final_url("https://staging.example.com/qa-login")
# Use final_url as pageurl for solver
Chạy hàm này một lần lúc khởi động job, cache kết quả rồi dùng URL cuối làm pageurl cho mọi task trong phiên.
Cách 4: đọc tên miền từ iframe reCAPTCHA
Khi widget render qua iframe, HTML đã chứa gợi ý về domain reCAPTCHA dùng để ràng buộc token:
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
Script chẩn đoán tên miền chạy trước khi gửi task
Gộp các kiểm tra trên vào một class nhỏ, chạy ở bước smoke test. Chi phí vài request, đổi lại bạn không đốt thread cho task chắc chắn trượt:
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()
Kết quả in ra đúng chuỗi cần dán vào pageurl, kèm các vấn đề phát hiện được.
Tình huống thực tế của đội QA tại Việt Nam
Ví dụ quen thuộc ở các công ty outsourcing TP.HCM và Đà Nẵng: đội QA nhận bàn giao hệ thống đặt lịch của khách, chạy regression đêm trên staging. Config ghi trang đăng nhập là staging.example.com/qa-login, nhưng hạ tầng khách hàng redirect sang một subdomain SSO riêng. Suite chạy ổn nhiều tuần, đến khi khách bật thêm tầng CDN thì tỷ lệ đăng nhập rớt một nửa — không exception, chỉ có test đỏ rải rác. Đúng như Lỗi 1: pageurl vẫn là URL cũ, token lại sinh ở hostname sau redirect. Cách sửa: gọi get_final_url() ở bước setup rồi đưa URL cuối vào config thay vì hardcode.
Vài lưu ý khác cho bối cảnh trong nước:
- Thông báo lỗi reCAPTCHA và phản hồi
siteverifyđều bằng tiếng Anh. Giữ nguyên chuỗi lỗi trong log và ticket; dịch sang tiếng Việt chỉ làm khó việc tra cứu. - Với hệ thống nhiều môi trường (dev, staging, UAT, production), tách
pageurlthành biến môi trường ngay từ đầu — đây là lỗi cấu hình, nên xử lý ở tầng cấu hình. - Nếu ghi log URL và hostname để audit, cân nhắc Nghị định 13/2023/NĐ-CP về bảo vệ dữ liệu cá nhân: chỉ lưu phần cần cho chẩn đoán.
- Vài trăm lượt giải mỗi đêm không cần nhiều thread. CaptchaAI tính tiền theo thread (luồng giải đồng thời), không theo từng lượt giải, nên gói BASIC ($15/tháng, 5 thread) đủ cho khối lượng này.
Câu hỏi thường gặp
pageurl phải là URL trước hay sau redirect?
URL sau redirect — nơi widget reCAPTCHA thực sự được render. Nếu staging.example.com/qa-login chuyển sang một subdomain đăng nhập riêng, đưa subdomain đó vào pageurl; chắc nhất là gửi request với allow_redirects=True rồi đọc response.url.
Token giải cho www.example.com dùng được trên example.com không?
Không nên trông vào đó. Hostname trong token là www.example.com, nên kết quả phụ thuộc hoàn toàn vào backend đích:
- Backend so khớp chính xác: từ chối ngay.
- Backend kiểm tra lỏng: cho qua, nhưng bạn không kiểm soát được.
- Nguyên tắc an toàn: giải token cho đúng biến thể sẽ submit.
Lỗi ERROR_WRONG_GOOGLEKEY có phải lỗi tên miền không?
Không. ERROR_WRONG_GOOGLEKEY là lỗi sitekey: googlekey sai định dạng hoặc lấy nhầm từ trang khác. Lỗi tên miền không lộ ra khi gửi task — nó chỉ xuất hiện sau khi bạn submit token và backend đích trả invalid-input-response.
Chạy staging trên localhost thì đặt pageurl thế nào?
localhost chỉ chạy được khi chủ site đã thêm localhost vào danh sách domain của site key, và token sinh ở đó không dùng lại được trên môi trường thật. Với automation, trỏ vào hostname staging truy cập được từ bên ngoài.
reCAPTCHA Enterprise có quy tắc tên miền khác không?
Cơ chế ràng buộc hostname giống nhau, nhưng danh sách domain nằm trong console theo từng key, và mỗi key thuộc một project. Token Enterprise bị từ chối thường do key gắn nhầm project hoặc domain chưa thêm vào key.
Chốt lại
pageurlbạn truyền cho CaptchaAI phải là domain thật sự nhận token, sau mọi redirect.- Thống nhất biến thể
wwwvà đi hết chuỗi redirect trước khi đưa URL vào config. - Ghi log hostname
siteverifytrả về và chạy script chẩn đoán trước khi mở suite hàng loạt.