وصول ردّ النداء (pingback) بنجاح لا يعني أن النتيجة وصلت إليك فعلاً. عندما يرسل CaptchaAI الحل إلى نقطة النهاية الخاصة بك، قد يكون خادمك متوقفاً أو مشغولاً أو يرد بخطأ — وحينها يُحلّ اختبار CAPTCHA بنجاح على الطرف الآخر بينما يختفي الحل من جانبك دون أي أثر. هذا الدرس يشرح ثلاثة أنماط تشغيلية تضمن ألا تفقد أي حل مهما تعطّل مسار التسليم: الاستطلاع الاحتياطي، وقائمة انتظار الرسائل الميتة، والمعالج المُحصّن ضد التكرار.
لماذا يفشل رد النداء أحياناً
ردّ النداء يوفّر الكثير مقارنة بالاستطلاع الدوري المتواصل، لكنه ينقل نقطة الفشل من كودك إلى شبكتك وخادمك. أي خلل بين لحظة انتهاء الحل ولحظة تخزينه لديك يعني حلاً ضائعاً. إليك أبرز الحالات:
| نوع الفشل | العَرَض | النتيجة |
|---|---|---|
| الخادم متوقف | يفشل اتصال CaptchaAI (connection refused) | لم يُسلَّم الحل |
| الخادم يرد بخطأ 5xx | يستقبل CaptchaAI استجابة خطأ | قد لا تُعاد المحاولة (حسب التنفيذ) |
| مهلة الشبكة تنتهي | يتعلّق اتصال CaptchaAI دون رد | احتمال ضياع الحل |
| المعالج ينهار | قُبِل الطلب لكن لم تُخزَّن النتيجة | يُفقد الحل دون إشعار |
القاعدة الأساسية واضحة: لا تعتمد على ردّ النداء وحده أبداً. اجعل له دائماً مساراً احتياطياً.
القيمة العملية: لماذا يهم الحل الضائع في الفوترة القائمة على الـ Thread
تخيّل منصة حجوزات في الخليج تعالج آلاف الطلبات في ساعات الذروة، كل طلب مرتبط باختبار CAPTCHA يجب حلّه قبل إتمام العملية. مع فوترة CaptchaAI القائمة على الـ Thread — حيث تدفع مقابل عدد الخيوط المتزامنة لا مقابل كل حل — فإن ضياع الحل لا يكلّفك رسماً إضافياً على الحل نفسه، لكنه يُهدر خيطاً كاملاً استُهلك على مهمة لن تصل نتيجتها، ويعطّل عملية المستخدم النهائي. على خطة مثل PREMIUM ($170 شهرياً، 100 Thread)، تعني كل مهمة ضائعة خيطاً أقل متاحاً لطلب حقيقي خلال الذروة. لذلك فإن متانة مسار رد النداء ليست ترفاً هندسياً بل حماية مباشرة لسعة المعالجة التي تدفع مقابلها.
أي نمط تختار؟
قبل الدخول في التفاصيل، إليك دليلاً سريعاً لاختيار النمط الأنسب لحجم حركة المرور واحتمالات الفشل لديك — ثم نشرح كل نمط بالتفصيل في الأقسام التالية. لست مضطراً لتطبيق الأنماط الثلاثة في كل مشروع:
| السيناريو | النمط الأنسب |
|---|---|
| حجم طلبات منخفض مع تعطّل عارض | ردّ النداء مع استطلاع احتياطي |
| حجم مرتفع واحتمال انقطاع قاعدة البيانات | قائمة انتظار الرسائل الميتة |
| احتمال معالجة عدة مستهلكين لنفس النتيجة | معالج idempotent |
| نظام إنتاجي مرتبط باتفاقيات مستوى خدمة | الأنماط الثلاثة معاً |
النمط الأول: ردّ النداء مع استطلاع احتياطي
الأسلوب الأكثر موثوقية هو قبول ردود النداء فور وصولها، مع تشغيل استطلاع دوري في الخلفية لأي مهمة لم يصلها ردّ نداء خلال مهلة محددة. بهذا يبقى ردّ النداء هو المسار السريع، ويعمل الاستطلاع كشبكة أمان تلتقط ما فاته.
بايثون
import os
import time
import threading
import requests
from flask import Flask, request
app = Flask(__name__)
API_KEY = os.environ["CAPTCHAAI_API_KEY"]
# Track task state
pending_tasks = {} # task_id -> {"submitted_at": timestamp, "status": "pending"}
results = {}
lock = threading.Lock()
def submit_captcha(sitekey, pageurl, callback_url):
"""Submit with callback, but track for fallback polling."""
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": pageurl,
"pingback": callback_url,
"json": 1
})
data = resp.json()
if data.get("status") == 1:
task_id = data["request"]
with lock:
pending_tasks[task_id] = {
"submitted_at": time.time(),
"status": "pending"
}
return task_id
return None
@app.route("/callback")
def captcha_callback():
"""Primary result delivery — CaptchaAI sends results here."""
task_id = request.args.get("id")
solution = request.args.get("code")
with lock:
results[task_id] = solution
pending_tasks.pop(task_id, None)
return "OK", 200
def fallback_poller():
"""Poll for any tasks that missed their callback."""
while True:
time.sleep(30) # Check every 30 seconds
with lock:
stale_tasks = [
tid for tid, info in pending_tasks.items()
if time.time() - info["submitted_at"] > 120 # 2 min callback timeout
and info["status"] == "pending"
]
for task_id in stale_tasks:
resp = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY,
"action": "get",
"id": task_id,
"json": 1
})
data = resp.json()
if data.get("status") == 1:
with lock:
results[task_id] = data["request"]
pending_tasks.pop(task_id, None)
print(f"Fallback poll recovered: {task_id}")
elif data.get("request") != "CAPCHA_NOT_READY":
# Permanent error — remove from pending
with lock:
pending_tasks.pop(task_id, None)
print(f"Task failed: {task_id} — {data.get('request')}")
# Start fallback poller in background
poller_thread = threading.Thread(target=fallback_poller, daemon=True)
poller_thread.start()
JavaScript
const express = require("express");
const axios = require("axios");
const app = express();
const API_KEY = process.env.CAPTCHAAI_API_KEY;
const pendingTasks = new Map(); // taskId -> { submittedAt, status }
const results = new Map();
async function submitCaptcha(sitekey, pageurl, callbackUrl) {
const resp = await axios.post("https://ocr.captchaai.com/in.php", null, {
params: {
key: API_KEY,
method: "userrecaptcha",
googlekey: sitekey,
pageurl: pageurl,
pingback: callbackUrl,
json: 1,
},
});
if (resp.data.status === 1) {
const taskId = resp.data.request;
pendingTasks.set(taskId, {
submittedAt: Date.now(),
status: "pending",
});
return taskId;
}
return null;
}
// Primary callback endpoint
app.get("/callback", (req, res) => {
const taskId = req.query.id;
const solution = req.query.code;
results.set(taskId, solution);
pendingTasks.delete(taskId);
res.sendStatus(200);
});
// Fallback poller
setInterval(async () => {
const now = Date.now();
const staleTasks = [];
for (const [taskId, info] of pendingTasks) {
if (now - info.submittedAt > 120000 && info.status === "pending") {
staleTasks.push(taskId);
}
}
for (const taskId of staleTasks) {
try {
const resp = await axios.get("https://ocr.captchaai.com/res.php", {
params: { key: API_KEY, action: "get", id: taskId, json: 1 },
});
if (resp.data.status === 1) {
results.set(taskId, resp.data.request);
pendingTasks.delete(taskId);
console.log(`Fallback recovered: ${taskId}`);
} else if (resp.data.request !== "CAPCHA_NOT_READY") {
pendingTasks.delete(taskId);
console.log(`Task failed: ${taskId} — ${resp.data.request}`);
}
} catch (err) {
console.error(`Poll error for ${taskId}: ${err.message}`);
}
}
}, 30000);
app.listen(3000);
مهلة الانتظار قبل بدء الاستطلاع الاحتياطي مهمة: امنح ردّ النداء وقتاً كافياً للوصول قبل أن تبدأ الاستفسار عن النتيجة، وإلا ستُرسل طلبات استطلاع لا داعي لها لمهام كانت ستصلك عبر ردّ النداء أصلاً.
النمط الثاني: قائمة انتظار الرسائل الميتة
المشكلة الأصعب ليست ضياع ردّ النداء، بل وصوله ثم فشل معالجتك له: قاعدة بيانات متوقفة، أو تحقق من الصحة يفشل، أو استثناء غير متوقع. في هذه الحالة لا ترمِ الحل — انقله إلى قائمة انتظار الرسائل الميتة (dead-letter queue) لتعيد معالجته لاحقاً بعد أن يعود النظام لطبيعته. لاحظ أنك تظل ترد بـ 200 لـ CaptchaAI حتى بعد الفشل الداخلي، لأن إعادة إرسال ردّ النداء ليست مضمونة.
بايثون
import json
import os
import time
from pathlib import Path
DEAD_LETTER_DIR = Path("dead_letter")
DEAD_LETTER_DIR.mkdir(exist_ok=True)
@app.route("/callback")
def captcha_callback_with_dlq():
task_id = request.args.get("id")
solution = request.args.get("code")
try:
# Attempt normal processing
store_result(task_id, solution)
return "OK", 200
except Exception as e:
# Processing failed — save to dead-letter queue
dead_letter = {
"task_id": task_id,
"solution": solution,
"error": str(e),
"received_at": time.time()
}
dlq_path = DEAD_LETTER_DIR / f"{task_id}.json"
dlq_path.write_text(json.dumps(dead_letter))
print(f"DLQ: {task_id} — {e}")
return "OK", 200 # Still return 200 to CaptchaAI
def reprocess_dead_letters():
"""Retry processing dead-letter items."""
for dlq_file in DEAD_LETTER_DIR.glob("*.json"):
item = json.loads(dlq_file.read_text())
try:
store_result(item["task_id"], item["solution"])
dlq_file.unlink() # Remove after successful processing
print(f"DLQ reprocessed: {item['task_id']}")
except Exception:
pass # Leave in DLQ for next retry
JavaScript
const fs = require("fs");
const path = require("path");
const DLQ_DIR = path.join(__dirname, "dead_letter");
if (!fs.existsSync(DLQ_DIR)) fs.mkdirSync(DLQ_DIR);
app.get("/callback-dlq", (req, res) => {
const taskId = req.query.id;
const solution = req.query.code;
try {
storeResult(taskId, solution);
res.sendStatus(200);
} catch (err) {
// Save to dead-letter queue
const deadLetter = {
task_id: taskId,
solution: solution,
error: err.message,
received_at: Date.now(),
};
fs.writeFileSync(
path.join(DLQ_DIR, `${taskId}.json`),
JSON.stringify(deadLetter)
);
console.log(`DLQ: ${taskId} — ${err.message}`);
res.sendStatus(200); // Still acknowledge to CaptchaAI
}
});
function reprocessDeadLetters() {
const files = fs.readdirSync(DLQ_DIR).filter((f) => f.endsWith(".json"));
for (const file of files) {
const filePath = path.join(DLQ_DIR, file);
const item = JSON.parse(fs.readFileSync(filePath, "utf8"));
try {
storeResult(item.task_id, item.solution);
fs.unlinkSync(filePath);
console.log(`DLQ reprocessed: ${item.task_id}`);
} catch (err) {
// Leave in DLQ
}
}
}
// Retry DLQ every 5 minutes
setInterval(reprocessDeadLetters, 300000);
الفكرة الجوهرية أن الفشل لا يساوي الضياع: كل حل يصل يُحفَظ على القرص فوراً، ثم تتولى مهمة دورية إعادة المعالجة حتى تنجح. هكذا يمكن لقاعدة البيانات أن تتوقف عشر دقائق دون أن تفقد حلاً واحداً.
النمط الثالث: معالج مُحصّن ضد التكرار (idempotent)
قد يصل ردّ النداء نفسه أكثر من مرة — بسبب إعادة إرسال، أو تسليم مزدوج، أو تسابق بين ردّ النداء والاستطلاع الاحتياطي. اجعل معالجك idempotent بحيث لا تؤثر المعالجة المكررة لنفس النتيجة على حالة نظامك:
@app.route("/callback")
def idempotent_callback():
task_id = request.args.get("id")
solution = request.args.get("code")
with lock:
# Only process if not already handled
if task_id in results:
return "OK", 200 # Already processed — skip silently
results[task_id] = solution
pending_tasks.pop(task_id, None)
return "OK", 200
عندما تجمع الأنماط الثلاثة معاً — استطلاع احتياطي يلتقط ما يضيع، وقائمة رسائل ميتة تلتقط ما يفشل، ومعالج idempotent يمتصّ التكرار — تحصل على مسار تسليم يصعب كسره حتى تحت الحمل العالي.
استكشاف الأخطاء وإصلاحها
| المشكلة | السبب | الإجراء |
|---|---|---|
| يُنشأ الرمز لكن الموقع المستهدف يرفضه | مفتاح الموقع أو الصفحة أو سياق الجلسة لا يتطابق | التقط المعلمات من جديد واستخدم الرمز داخل جلسة HTTP أو المتصفح نفسها |
| ينتهي الاستطلاع بمهلة قبل وصول النتيجة | الفاصل الزمني أو مهلة الانتظار أضيق من اللازم | استطلع كل 5 إلى 10 ثوانٍ وافصل بين انتهاء المهلة والأخطاء الفعلية وسجّل السبب |
| ينجح المثال محلياً ويفشل داخل سير العمل | حقن الرمز أو حقل النموذج أو ردّ النداء مفقود في السلسلة الحقيقية | تحقق من المسار الكامل بين مزوّد الحل والطلب النهائي إلى الموقع المستهدف |
| الاستطلاع الاحتياطي يلتقط مهام سُلّمت أصلاً | تسابق بين ردّ النداء والاستطلاع | فعّل فحص idempotent وتجاهل المهمة إن كانت موجودة في النتائج |
أسئلة شائعة
كيف أفرّق بين ردّ نداء متأخر وردّ نداء ضائع فعلاً؟
لا تعتبر المهمة ضائعة لمجرد تأخرها. سجّل وقت الإرسال لكل مهمة، ولا تفعّل الاستطلاع الاحتياطي إلا بعد تجاوز مهلة مريحة (نحو 120 ثانية) دون وصول ردّ النداء. المهمة التي يجيب عنها الاستطلاع بـ CAPCHA_NOT_READY لا تزال قيد الحل، أما التي تعيد خطأً دائماً فهي فاشلة فعلاً ويجب إزالتها من قائمة الانتظار.
هل أخزّن نتيجة ردّ النداء قبل أن أرد بـ 200 أم بعده؟
خزّن النتيجة أولاً ثم ردّ بـ 200. إذا انهار المعالج بعد إرسال الرد وقبل حفظ الحل، فسيظن CaptchaAI أن التسليم نجح ولن يعيد المحاولة، ويضيع الحل بصمت. المعالجة قبل الاستجابة — أو استخدام نمط قائمة الرسائل الميتة — تحميك من هذه الفجوة.
ما أثر فشل ردّ النداء على فوترة CaptchaAI القائمة على الـ Thread؟
الفوترة قائمة على عدد الخيوط المتزامنة مع حلول غير محدودة لكل خيط، لذا لا يفرض الحل الضائع رسماً إضافياً على الحل نفسه. الكلفة الحقيقية تشغيلية: خيط استُهلك على مهمة لن تصل نتيجتها، وعملية مستخدم نهائي تعطّلت. أنماط الاسترداد هنا تحمي سعة المعالجة التي تدفع مقابلها لا فاتورتك المباشرة.
كيف أختبر منطق الاسترداد دون انتظار فشل حقيقي؟
حاكِ الفشل عمداً: أوقف نقطة نهاية ردّ النداء مؤقتاً وتحقق من أن الاستطلاع الاحتياطي يلتقط المهام، أو اجعل دالة store_result ترمي استثناءً للتأكد من وصول العنصر إلى قائمة الرسائل الميتة ثم إعادة معالجته. اختبار مسارات الفشل قبل الإنتاج أرخص بكثير من اكتشافها أثناء الذروة.
الخطوات التالية
- البدء السريع مع CaptchaAI: حلّ أول كابتشا في 5 دقائق
- كيفية حلّ reCAPTCHA v2 عبر الـ API: دليل خطوة بخطوة
- كيفية حل Cloudflare Turnstile باستخدام واجهة API
- كيفية حل GeeTest v3 باستخدام API