استكشاف الأخطاء

انتهاء صلاحية رمز reCAPTCHA: توقيت النوافذ وظروف السباق

عمر رمز reCAPTCHA هو 120 ثانية، والعدّاد يبدأ لحظة اكتمال الحل — لا لحظة وصول الرمز إلى سكربتك. هذه التفصيلة وحدها تفسّر أغلب رسائل timeout-or-duplicate في سير عمل كان يعمل أمس.

والنافذة سخية في الواقع: بعد خصم زمن الشبكة والاستطلاع الدوري يبقى أمامك 105 إلى 117 ثانية. المشكلة ليست ضيق الوقت، بل ترتيب الخطوات داخل الكود.

الخطأ نفسه وسببان مختلفان

ما تراه ما حدث فعليًا ما تفعله
فشل بعد انتظار طويل مرّ أكثر من 120 ثانية قبل الإرسال رمز جديد، ومسافة أقصر بين الحل والإرسال
فشل بعد ثوانٍ من الحل أُرسل الرمز نفسه مرتين رمز واحد لكل إرسال، دون إعادة استخدام

نافذة 120 ثانية: أين تبدأ ومتى تنتهي

قبل تقصير المسافة بين الحل والإرسال، حدّد نقطة الصفر:

نمط التكامل يبدأ العدّاد عند...
مربع الاختيار في reCAPTCHA v2 تأشير المستخدم على المربع
شبكة الصور في reCAPTCHA v2 آخر جولة اختيار للصور
reCAPTCHA v2 غير المرئي إطلاق execute() ومعه الرمز
reCAPTCHA v3 تحقّق الـ Promise من execute()
الحل عبر الـ API — CaptchaAI إنشاء الخدمة للرمز، لا استلامك له

وعلى الخط الزمني تبدو الدورة هكذا في كل الإصدارات:

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)

فجوة التسليم بين الخدمة وسكربتك

العدّاد يعمل بين إنشاء الرمز ووصوله إليك عبر res.php، وتلك الثواني محسوبة من الـ 120:

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)

يبقى الهامش مريحًا طالما أرسلت الرمز فور استلامه. التوتر يبدأ حين:

  • تفصل خطوات معالجة بين الاستلام والإرسال
  • يلزم استكمال أكثر من نموذج قبل الإرسال
  • يتباطأ الموقع المستهدف في ساعات الذروة
  • تُخزَّن الرموز في قائمة انتظار لاستخدامها لاحقًا

ثلاث حالات سباق تلتهم النافذة

1. حلّ متوازٍ ثم إرسال متسلسل

النمط الشائع: إطلاق عمليات الحل دفعة واحدة ثم إرسال النتائج واحدة تلو الأخرى؛ ينجح الرمز الأول بينما يشيخ الأخير قبل أن يبلغ النموذج.

# 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

الإصلاح: أغلق دورة كل تحدٍّ قبل فتح التالي — حلّ، ثم أرسل، ثم انتقل:

# 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)

2. تجهيز الرمز قبل أوانه

الجلب المسبق يبدو تحسينًا للأداء، لكنه يجعل الرمز يشيخ قبل أن يعرف أحد متى سيُستخدم.

# 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!

الإصلاح: اجعل الحل آخر خطوة، وأنجز التحقق من المدخلات قبل إنفاق رمز:

# 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)

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...

القاعدة العملية: أي خطوة تنتظر طرفًا ثالثًا — تأكيد ببريد أو رسالة — تُلغي كل افتراض عن عمر الرمز.

الإصلاح: قِس عمر الرمز، وأعد الحل إذا تجاوز حدّ الأمان:

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)

من الميدان: اختبارات ليلية على مسار الدفع

فريق QA في متجر إلكتروني بالرياض يشغّل اختبارات ليلية معتمدة على بيئة الاختبار الخاصة به: تسجيل دخول، ثم إتمام الشراء خلف reCAPTCHA v2. كان السكربت يحلّ عشرين تحديًا مسبقًا ثم يمرّ على السيناريوهات بالترتيب، فتفشل الحالات الأخيرة كل ليلة. نقل الحل إلى ما قبل الإرسال أنهى المشكلة، وخرج الفريق بثلاث ملاحظات:

  • في مواسم التخفيضات يبطؤ رد بوابة الدفع فتضيق النافذة من طرفها الآخر؛ وسّع الهامش قبل الموسم
  • سجّل عمر كل رمز لحظة الإرسال — بدونه يصبح التشخيص تخمينًا
  • الفوترة في CaptchaAI على أساس عدد الـ threads المتزامنة: الترقية من BASIC بـ $15 شهريًا و5 threads إلى ADVANCE بـ $90 و50 thread توسّع التوازي، لا عمر الرمز

نمط جاهز: مدير يقيس عمر الرمز

بدل تكرار حسابات التوقيت في كل سكربت، اجمعها في فئة تعرف متى يصلح الرمز ومتى يُطلب غيره:

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://example.com/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...

تشخيص timeout-or-duplicate من عمر الرمز

عمر الرمز لحظة الفشل يكفي لتحديد السبب: أكثر من 120 ثانية يعني انتهاء صلاحية، وأقل من خمس ثوانٍ يعني تكرارًا:

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"}

قواعد التوقيت في جدول واحد

راجع الجدول عند تدقيق أي سكربت قائم:

الممارسة التوصية
المسافة بين الحل والإرسال تحت 90 ثانية
فاصل الاستطلاع الدوري 5 ثوانٍ على res.php
الحل المسبق فقط مع إرسال خلال 60 ثانية
عند timeout-or-duplicate رمز جديد فورًا
تخزين الرموز لا قوائم انتظار
التشغيل المتوازي حلّ لكل مهمة، لا حلّ جماعي
المراقبة سجّل عمر الرمز لحظة الإرسال

تعيد Google رمز الخطأ نفسه في الحالتين عمدًا حتى لا يستدلّ السكربت على السبب من الاستجابة. والعلاج واحد.

أسئلة شائعة

كم ثانية تبقى لي فعليًا بعد استلام الرمز؟

بفاصل استطلاع مقداره 5 ثوانٍ قد يصلك الرمز بعد 5 ثوانٍ من إنشائه، فيبقى أمامك نحو 115 ثانية. أضف زمن معالجتك وزمن استجابة الموقع، وستجد أن 90 ثانية سقف عملي آمن.

الرمز حديث ومع ذلك يفشل التحقق — ما الخطوة التالية؟

راجع هذه النقاط قبل أن تتهم التوقيت:

  • اسم الحقل g-recaptcha-response والقيمة تصل كاملة دون اقتطاع
  • sitekey وعنوان الصفحة يطابقان الصفحة الحقيقية
  • لا إرسال مزدوج في معالج النموذج؛ النقرة المزدوجة تستهلك الرمز مرتين

هل يمكن إرسال الرمز نفسه مرة ثانية بعد فشل المحاولة الأولى؟

لا. الرمز يُستهلك عند أول تحقق، وأي إرسال لاحق يعود بالخطأ نفسه. عالج إعادة المحاولة برمز جديد.

هل يختلف عمر الرمز بين reCAPTCHA v2 وv3 وEnterprise؟

النافذة 120 ثانية في الإصدارات كلها؛ الفرق في طريقة توليد الرمز لا في مدته. ما يتغير هو زمن الحل، وهو يُحسب ضمن الهامش.

ما هامش الأمان المناسب قبل إعادة الحل؟

ابدأ من 15 ثانية كما في المدير أعلاه، ووسّعه إلى 30 مع موقع بطيء أو شبكة متذبذبة. الهامش الأوسع يكلّفك عمليات حل إضافية، لكنه أرخص من دورة فشل كاملة.

الخلاصة

الرمز يعيش 120 ثانية ويُستهلك مرة واحدة، وكل رسالة timeout-or-duplicate تعني أنك تأخرت أو كرّرت. مع CaptchaAI اجعل الحل آخر خطوة قبل الإرسال، وسجّل عمر الرمز، ولا تخزّنه في قوائم انتظار.

مقالات ذات صلة

أدلة ذات صلة

التعليقات غير مفعّلة لهذا المقال.