Khắc Phục Sự Cố

Token reCAPTCHA hết hạn: cửa sổ 120 giây và các race condition

Đồng hồ 120 giây của token reCAPTCHA không bắt đầu ở máy bạn: nó chạy từ lúc token được tạo, trước cả khi res.php trả kết quả về. Nên khi log hiện timeout-or-duplicate, chỉ có hai khả năng — token quá hạn lúc site đích xác thực, hoặc cùng một chuỗi token đi qua handler hai lần.

Cửa sổ đó không nới ra được — Google chốt cứng ở phía server. Phần bạn kiểm soát là khoảng cách giải → gửi. Bài mở đầu bằng bảng quy tắc cho pipeline.


Bảng quy tắc canh giờ token cho pipeline tự động

Hạng mục Khuyến nghị
Khoảng cách giải → gửi Giữ dưới 90 giây, chừa biên an toàn 30 giây
Khoảng polling 5 giây giữa hai lần gọi res.php
Lấy token sớm Chỉ khi chắc chắn form gửi trong 60 giây
Khi gặp timeout-or-duplicate Luôn xin token mới, không gửi lại token cũ
Hàng đợi token Không xếp hàng token — mỗi token hết hạn độc lập
Chạy song song Mỗi worker tự giải, tự gửi; không gom token dùng chung
Giám sát Log tuổi token lúc submit để thấy xu hướng sớm

Dòng cuối rẻ nhất mà hay bị bỏ qua nhất: trường token_age ghi kèm mỗi lần submit cho thấy pipeline trôi dần về mốc 120 giây từ nhiều tuần trước. Team nào đã giữ log kiểm toán theo Nghị định 13/2023/NĐ-CP thì chỉ tốn thêm một trường.


Token reCAPTCHA hết hạn sau bao lâu?

Mọi token reCAPTCHA hết hạn đúng 120 giây sau khi được tạo, giống nhau ở cả v2 lẫn v3.

Đồng hồ bắt đầu chạy từ mốc nào

Mốc bắt đầu tuỳ theo cách thử thách được giải, và đây là chỗ hay tính nhầm:

Kịch bản Đồng hồ chạy từ lúc...
reCAPTCHA v2 checkbox Người dùng tick vào ô (thử thách đã giải xong)
reCAPTCHA v2 lưới hình ảnh Người dùng chọn xong vòng ảnh cuối cùng
reCAPTCHA v2 invisible Callback execute() trả về token
reCAPTCHA v3 Promise execute() resolve kèm token
Solver API (CaptchaAI) Solver tạo ra token — không phải lúc bạn nhận được nó

Cửa sổ 120 giây trên trục thời gian

Token generated (solve complete)
    ├─── 0s:   Token is valid ✓
    ├─── 60s:  Token is valid ✓
    ├─── 110s: Token is valid ✓  (but cutting it close)
    ├─── 119s: Token is valid ✓  (dangerous territory)
    └─── 120s: Token EXPIRED ✗  (timeout-or-duplicate)

Từ mốc 110 giây trở đi là vùng nguy hiểm: request còn phải qua site đích rồi tới Google.

Khoảng trễ giữa solver và code của bạn

Luôn có độ trễ giữa lúc token được tạo và lúc code của bạn cầm được nó:

Solver generates token (timer starts)
    ↓ ~1-5 seconds (network + polling interval)
Your code receives token via res.php poll
    ↓ You now have ~115-119 seconds remaining
Your code processes and submits token
    ↓ ~1-10 seconds (depends on your workflow)
Target website validates token with Google
    ↓ ~1-2 seconds (Google API response time)
Total remaining after validation: ~105-117 seconds (comfortable)

Polling mỗi 5 giây khiến bạn nhận token muộn tối đa 5 giây — còn khoảng 115 giây, gửi ngay thì vẫn thoải mái. Rắc rối bắt đầu khi có thứ chen vào giữa:

Thứ chen vào giữa Ảnh hưởng lên biên an toàn
Vài bước xử lý sau khi nhận token Mỗi bước cộng thêm giây
Biểu mẫu hoặc bước xác nhận phụ Thời gian khó dự đoán
Site đích chậm hoặc kết nối quốc tế Round-trip dài, cộng dồn
Hàng đợi token rồi gửi dần Token cuối hàng chết trước

Dòng thứ ba đáng chú ý với team ở Việt Nam: worker đặt trong nước mà site đích ở Mỹ hay EU thì mỗi request mất thêm vài trăm mili giây, nhân lên qua vài lần gọi mạng.


Ba race condition làm token chết trước lúc dùng

Race condition 1: giải song song nhưng gửi tuần tự

Lỗi phổ biến nhất trong job scraping theo lô: bắn hết task lên in.php, rồi mới gửi form lần lượt.

# WRONG: Solving multiple CAPTCHAs in parallel, then submitting sequentially
tokens = []
for url in urls:
    task_id = solve_captcha(url)  # All submitted at t=0
    tokens.append(task_id)

# All tokens arrive around t=30
solved_tokens = [poll_result(tid) for tid in tokens]

# Sequential submission: first token at t=32, last at t=120+
for i, (url, token) in enumerate(zip(urls, solved_tokens)):
    submit_form(url, token)  # Later tokens may be expired!
    time.sleep(10)  # Each wait adds pressure

Token cùng ra lò quanh giây thứ 30 nhưng token cuối chỉ được gửi sau giây 120 — đã chết. Cách sửa: giải xong cái nào gửi ngay cái đó.

# CORRECT: Solve and submit one at a time
for url in urls:
    token = solve_and_wait(url)  # Token received at t=30
    submit_form(url, token)      # Submitted at t=31 (89 seconds remaining)

Đổi thứ tự không mất throughput, vì CaptchaAI tính tiền theo thread (luồng giải đồng thời) chứ không theo lượt giải: gói STANDARD ($30/tháng, 15 thread) cho 15 worker chạy song song, mỗi worker tự giải tự gửi. Tốc độ nằm ở số thread, không ở việc gom token thành lô.

Race condition 2: lấy token quá sớm

# WRONG: Pre-fetching tokens before knowing when they'll be used
token = solve_captcha()  # Token received at t=0

# ... user fills out form (30-120+ seconds) ...
# ... validation checks ...
# ... other processing ...

submit_form(token)  # Token may be expired!

Cách sửa: đẩy bước giải CAPTCHA xuống sát lệnh submit. Việc không cần token — chuẩn bị dữ liệu, validate input — làm trước:

# CORRECT: Late-bind the CAPTCHA solve
prepare_form_data()   # Do everything that doesn't need the token
validate_inputs()     # Run validation before spending a token

# Now solve and submit immediately
token = solve_captcha()  # Token received at t=0
submit_form(token)       # Submitted at t=1 (119 seconds remaining)

Race condition 3: form nhiều bước

Hay gặp khi QA luồng đăng ký hoặc đặt lịch hẹn: CAPTCHA ở bước 1, còn lệnh submit thật ở bước 3.

# PROBLEMATIC: Multi-step form where CAPTCHA is on step 1 but submit is step 3
token = solve_captcha()       # t=0: Token received

fill_step_1(token)            # t=5: Step 1 submitted
response = fill_step_2()      # t=15: Step 2 completed
# ... step 2 has additional verification ...
wait_for_verification()       # t=60: Verification complete
fill_step_3_and_submit()      # t=65: Final submission (55 seconds remaining - OK)
# BUT if step 2 takes longer than expected...

Nhịp bình thường vẫn kịp, nhưng chỉ cần bước 2 chậm hơn dự kiến — thêm một lần OTP, một trang xác nhận tải lâu — là token chết. Cách sửa: đo tuổi token trước lệnh submit cuối, già thì giải lại:

token_received_at = time.time()
token = solve_captcha()
token_received_at = time.time()

# ... multi-step process ...

# Before final submission, check token age
token_age = time.time() - token_received_at
if token_age > 100:  # 20-second safety margin
    print(f"Token is {token_age:.0f}s old — requesting fresh token")
    token = solve_captcha()
    token_received_at = time.time()

submit_final(token)

Gói toàn bộ logic canh giờ vào một class

Ba cách sửa trên quy về một việc: luôn biết token bao nhiêu giây tuổi. Thay vì rải time.time() khắp code, gói nó vào một class tự ghi mốc nhận token và giải lại khi còn dưới 15 giây:

import time
import requests

class TokenTimingManager:
    """Manage reCAPTCHA token timing to prevent expiration errors."""

    API_KEY = "YOUR_API_KEY"
    TOKEN_LIFETIME = 120
    SAFETY_MARGIN = 15  # seconds before expiry to consider "stale"

    def __init__(self, site_key, page_url, version="v2"):
        self.site_key = site_key
        self.page_url = page_url
        self.version = version
        self.current_token = None
        self.token_timestamp = None

    def _solve(self):
        """Request and poll for a new token."""
        params = {
            "key": self.API_KEY,
            "method": "userrecaptcha",
            "googlekey": self.site_key,
            "pageurl": self.page_url,
            "json": 1,
        }
        if self.version == "v3":
            params.update({"version": "v3", "action": "submit"})

        submit = requests.post("https://ocr.captchaai.com/in.php", data=params).json()
        task_id = submit["request"]

        for _ in range(60):
            time.sleep(5)
            result = requests.get("https://ocr.captchaai.com/res.php", params={
                "key": self.API_KEY,
                "action": "get",
                "id": task_id,
                "json": 1,
            }).json()

            if result.get("status") == 1:
                self.current_token = result["request"]
                self.token_timestamp = time.time()
                return self.current_token

        raise TimeoutError("Token solve timeout")

    @property
    def token_age(self):
        """Seconds since current token was received."""
        if self.token_timestamp is None:
            return float("inf")
        return time.time() - self.token_timestamp

    @property
    def token_remaining(self):
        """Seconds remaining before token expires."""
        return max(0, self.TOKEN_LIFETIME - self.token_age)

    @property
    def is_fresh(self):
        """Whether the token is fresh enough to use."""
        return self.token_remaining > self.SAFETY_MARGIN

    def get_token(self):
        """Get a valid token, solving if current is stale or missing."""
        if self.current_token and self.is_fresh:
            return self.current_token

        return self._solve()

    def use_token(self):
        """Get and consume a token (cannot be reused)."""
        token = self.get_token()
        # Mark as consumed
        self.current_token = None
        self.token_timestamp = None
        return token

# Usage
manager = TokenTimingManager(
    site_key="6LcR_RsTAAAAAN_r0GEkGBfq3L7KmU5JbPHJtwNp",
    page_url="https://staging.example.com/qa-login",
)

# Get a fresh token right before submission
token = manager.use_token()
print(f"Token remaining: {manager.TOKEN_LIFETIME}s (fresh solve)")

# Submit form with token...

use_token() xoá token khỏi bộ nhớ ngay sau khi trả về: token chỉ dùng một lần, giữ lại "cho chắc" là nửa còn lại của lỗi.


Đọc mã lỗi timeout-or-duplicate: hết hạn hay trùng lặp?

Google dùng chung một mã lỗi cho hai nguyên nhân, nên phải suy ra từ tuổi token lúc gửi:

def handle_recaptcha_error(error_codes, token_age_seconds):
    """Diagnose and handle reCAPTCHA validation errors."""

    if "timeout-or-duplicate" in error_codes:
        if token_age_seconds > 120:
            return {
                "cause": "Token expired (age: {:.0f}s > 120s)".format(token_age_seconds),
                "fix": "Reduce time between receiving and submitting token",
                "action": "re-solve",
            }
        elif token_age_seconds < 5:
            return {
                "cause": "Token likely reused (duplicate submission)",
                "fix": "Ensure each form submission gets a unique token",
                "action": "re-solve",
            }
        else:
            return {
                "cause": "Token may have been reused or server-side timing issue",
                "fix": "Check for double-submit in form handler",
                "action": "re-solve",
            }

    if "invalid-input-response" in error_codes:
        return {
            "cause": "Token is malformed or corrupted",
            "fix": "Check token transmission (URL encoding, field name)",
            "action": "re-solve",
        }

    return {"cause": "Unknown", "action": "investigate"}
Tuổi token lúc gửi Nguyên nhân Việc cần làm
Trên 120 giây Token đã hết hạn Rút ngắn quãng giải → submit
Dưới 5 giây Cùng token gửi hai lần Chặn double-submit ở handler form
5–120 giây Trùng lặp hoặc lệch giờ server Soát retry của HTTP client

Cả ba nhánh chung một hành động: xin token mới, không gửi lại chuỗi cũ.


Câu hỏi thường gặp

Tuổi token bao nhiêu thì nên giải lại thay vì gửi?

Trên 90 giây. Ngưỡng đó chừa 30 giây cho request tới site đích và cho Google xác thực, kể cả khi mạng chậm.

Vì sao token vẫn báo lỗi dù mới vài giây tuổi?

Gần như luôn là gửi trùng: cùng một token đi qua handler hai lần, thường do double-submit ở nút bấm hoặc retry của HTTP client.

Worker ở Việt Nam, site đích ở Mỹ thì chừa thêm bao nhiêu biên?

Đừng đoán, hãy đo trên pipeline của bạn:

  • Ghi token_age ở mọi lần submit trong một tuần.
  • Lấy phân vị 95 làm biên thật, không lấy trung bình.
  • Vượt 90 giây thì cắt bớt bước xử lý nằm giữa, đừng nâng ngưỡng.

Quy tắc này có khác nhau giữa reCAPTCHA v2 và v3 không?

Không. Cửa sổ 120 giây và quy tắc dùng một lần áp dụng cho cả hai. Khác biệt duy nhất là mốc bắt đầu: với v3, đồng hồ chạy từ khi Promise execute() resolve.


Tóm lại

Token hết hạn sau đúng 120 giây và chỉ dùng một lần — hai đặc tính đó đứng sau gần như mọi ca timeout-or-duplicate. Ba việc cần làm:

  1. Giải token ở bước cuối, ngay sát lệnh submit.
  2. Đo tuổi token và giải lại khi vượt 90 giây.
  3. Không gom token vào hàng đợi; mỗi worker tự giải tự gửi.

Class TokenTimingManager gói sẵn cả ba. Với CaptchaAI, giữ đúng nhịp in.php → polling res.php → gửi form ngay là đủ.

Bài viết liên quan

Os comentários estão desativados para este artigo.