“يا زلمة، كمان دقيقة وبنسأل كمان مرة”… قصة إفلاس حصتي من الـ API
يا جماعة الخير، اسمحوا لي أرجع بالزمن كم سنة لورا. كنت شغال على مشروع متجر إلكتروني لأحد العملاء، وكان جزء أساسي من المشروع هو التكامل مع شركة شحن خارجية. الفكرة بسيطة: لما يتغير وضع الطلب في نظام شركة الشحن (مثلاً: “قيد التجهيز”، “تم الشحن”، “في الطريق للتسليم”)، لازم نحدّث الحالة فوراً في قاعدة بياناتنا عشان العميل يشوف التحديث في حسابه.
في البداية، كأي مبرمج متحمس، قلت “بسيطة!”. الحل الأول اللي خطر ببالي كان الـ Polling أو ما أحب أن أسميه “الاستقصاء المستمر”. كتبت سكربت بسيط، بلغة بايثون وقتها، كل دقيقة يروح يسأل الـ API تبع شركة الشحن: “يا هوو، شو صار في الطلب رقم 123؟”، “طيب والطلب 124؟”، وهكذا لكل الطلبات الفعالة.
في أول يومين، كانت الأمور تمام والعميل مبسوط. لكن مع زيادة عدد الطلبات، بدأت الكارثة. السيرفر صار يلهث، استهلاك الموارد في السماء، وفواتير استدعاء الـ API بدأت تتضخم. الأسوأ من هيك، في يوم من الأيام صحيت على إيميل من شركة الشحن نصه: “لقد تجاوزت الحد المسموح به من استدعاءات الـ API، تم حظر حسابك مؤقتاً”. يا ويلي! شعرت كأني أستجدي المعلومة منهم، وهم في النهاية طردوني. كانت “وجعة راس” حقيقية، وكنت على وشك خسارة المشروع بسبب هذه المشكلة التقنية البسيطة في ظاهرها والمعقدة في أثرها. هنا كانت بداية رحلتي للبحث عن حل أفضل، حل أنقذني من هذا الجحيم، وهو ما يُعرف بالـ Webhooks.
ما هو جحيم الاستقصاء (Polling Hell)؟
قبل ما نحكي عن المنقذ، خلونا نفصّل أكتر في المشكلة. الـ Polling هو أن يقوم تطبيقك بشكل دوري (كل ثانية، كل دقيقة، كل ساعة) بإرسال طلب HTTP إلى خادم آخر ليسأله: “هل هناك أي تحديثات؟”.
تخيل أنك طفل في المقعد الخلفي للسيارة في رحلة طويلة، وكل دقيقة تسأل والدك: “هل وصلنا؟”. هذا هو الـ Polling بالضبط. مزعج، غير فعال، ويستهلك طاقة وصبر والدك (الخادم).
هذه الطريقة، رغم بساطتها، لها عيوب قاتلة في كثير من السيناريوهات:
إهدار الموارد (Wasted Resources)
في 99% من الحالات، سيكون جواب الخادم: “لا، لا يوجد جديد”. هذا يعني أنك أهدرت موارد الشبكة (Bandwidth)، وقوة المعالجة (CPU) على خادمك، وحصة استدعاءات الـ API الثمينة (API Quota) فقط لتسمع “لا”. إنه كمن يتصل بصديقه مئة مرة ليطمئن عليه، بينما كان يستطيع انتظار رسالة منه عند حدوث شيء مهم.
التأخير في استلام البيانات (Data Latency)
أنت لا تحصل على التحديث لحظة وقوعه، بل تحصل عليه فقط عندما يحين دور طلبك التالي. لو كنت تسأل كل دقيقة، وحدث التغيير في الثانية الأولى بعد آخر سؤال، فستنتظر 59 ثانية كاملة لتعرف بوجود التحديث. في عالم اليوم، هذا التأخير قد يكون غير مقبول إطلاقاً.
عبء على الخوادم (Server Load)
أنت لا ترهق خادمك فقط، بل ترهق خادم الطرف الثالث أيضاً. هذا هو السبب الذي يدفع معظم الخدمات لوضع حدود قاسية على عدد الطلبات (Rate Limiting) لمنع استنزاف مواردهم من قبل تطبيقات تستخدم الـ Polling بكثرة.
الـ Webhooks: المنقذ الذي يطرق بابك حين يجهز الطلب
الآن نأتي للحل السحري. الـ Webhook هو عكس الـ API التقليدي تماماً. بدلاً من أن يقوم تطبيقك بالسؤال، تقوم أنت بإعطاء الطرف الثالث “عنوان اتصال” (URL) خاص بك، وهو يقوم بإخبارك عند حدوث شيء جديد.
باستخدام نفس مثال الرحلة، الـ Webhook هو أن يقول لك والدك: “اهدأ يا بني، عندما نصل سأخبرك بنفسي”. أنت الآن تستطيع النوم، اللعب، أو الاستمتاع بالطريق (أي أن يقوم خادمك بمهام أخرى مفيدة) بدلاً من السؤال المستمر.
يُطلق على هذا المفهوم اسم “Reverse API” أو “HTTP Push API”، وهو حجر الأساس لما يُعرف بـ “البنية الموجهة بالأحداث” (Event-Driven Architecture). الآلية بسيطة وعبقرية:
- التسجيل (Registration): أنت تذهب إلى إعدادات حسابك في الخدمة الخارجية (مثل بوابة الدفع، شركة الشحن، GitHub) وتضيف عنوان URL من تطبيقك في قسم الـ Webhooks. هذا الـ URL يسمى “Webhook Endpoint”.
- الحدث (Event): يحدث شيء ما في الخدمة الخارجية. مثلاً، عميل يدفع فاتورة، أو يتم شحن طلبية.
- الإشعار (Notification): تقوم الخدمة الخارجية تلقائياً بإرسال طلب HTTP (غالباً من نوع
POST) إلى الـ URL الذي سجلته. هذا الطلب يحتوي على كل تفاصيل الحدث في جسمه (Payload)، غالباً بصيغة JSON. - المعالجة (Processing): يستقبل تطبيقك هذا الطلب، يقرأ البيانات، ويقوم بالإجراء اللازم (تحديث قاعدة البيانات، إرسال إيميل، …إلخ).
لنطبّق عملياً: من الـ Polling إلى الـ Webhooks
الحكي النظري جميل، لكن خلينا نشوف الكود. سأستخدم Node.js مع إطار Express كمثال، لكن المبدأ نفسه ينطبق على أي لغة أو إطار عمل.
مثال (1): شيفرة الـ Polling البائسة
هكذا كان يبدو الكود القديم الذي سبب لي “وجعة الراس”:
// Polling example using Node.js and axios
const axios = require('axios');
const THIRD_PARTY_API = 'https://shipping-company.com/api/v1/orders/';
const API_KEY = 'your_secret_api_key';
// قائمة بالطلبات التي ننتظر تحديثها
const activeOrders = ['order_123', 'order_124', 'order_125'];
async function checkOrderStatus(orderId) {
try {
const response = await axios.get(`${THIRD_PARTY_API}${orderId}`, {
headers: { 'Authorization': `Bearer ${API_KEY}` }
});
console.log(`Status for ${orderId}: ${response.data.status}`);
// هنا نكتب منطق تحديث قاعدة البيانات...
} catch (error) {
console.error(`Failed to get status for ${orderId}:`, error.message);
}
}
// ابدأ الجحيم: اسأل عن كل طلب كل 60 ثانية
setInterval(() => {
console.log('Checking for updates...');
activeOrders.forEach(orderId => {
checkOrderStatus(orderId);
});
}, 60000); // 60000 ms = 1 minute
لاحظ كيف أننا نقوم بحلقة لا نهائية من الطلبات، معظمها سيعود بلا فائدة.
مثال (2): إعداد مستقبل Webhook (Endpoint)
الآن، لنرى الطريقة الأنيقة والفعالة. سنقوم ببناء سيرفر بسيط يستمع للطلبات القادمة من شركة الشحن.
// Webhook endpoint example using Node.js and Express
const express = require('express');
const app = express();
// Middleware مهم جداً لقراءة جسم الطلب بصيغة JSON
app.use(express.json());
const PORT = 3000;
// هذا هو الـ URL الذي سنعطيه لشركة الشحن
// e.g., https://your-app.com/webhooks/shipping-updates
app.post('/webhooks/shipping-updates', (req, res) => {
const eventPayload = req.body;
console.log('🎉 Received a new shipping update!');
console.log(eventPayload);
// مثال على البيانات القادمة
// {
// "event_type": "order_shipped",
// "data": {
// "order_id": "order_123",
// "new_status": "Shipped",
// "tracking_number": "SH123456789"
// }
// }
const { order_id, new_status } = eventPayload.data;
// هنا نكتب منطق تحديث قاعدة البيانات
// مثلاً: updateOrderStatusInDB(order_id, new_status);
console.log(`Updating order ${order_id} to status: ${new_status}`);
// مهم جداً: أرسل استجابة 200 OK لتُعلِم الخدمة أنك استلمت الإشعار بنجاح
res.status(200).send('Received');
});
app.listen(PORT, () => {
console.log(`Server listening for webhooks on port ${PORT}`);
});
لاحظ الفرق الشاسع. الكود الآن لا يفعل شيئاً، هو فقط “يستمع”. لا يوجد استهلاك للموارد إلا عند وصول تحديث حقيقي. الكفاءة هنا في أعلى مستوياتها.
نصيحة من الخبير: تأمين الـ Webhook الخاص بك!
هناك مشكلة أمنية خطيرة: أي شخص يعرف عنوان الـ Endpoint الخاص بك يستطيع إرسال طلبات مزيفة إليه. تخيل أن منافساً يقوم بتحديث كل طلباتك إلى “تم الإلغاء”! لتجنب هذه الكارثة، يجب دائماً التحقق من مصدر الـ Webhook.
الطريقة المعتمدة هي استخدام “التوقيع” (Signature). تقوم الخدمة الخارجية بتوقيع كل طلب ترسله باستخدام مفتاح سري (Secret) تشاركه معك فقط. مهمتك هي التحقق من صحة هذا التوقيع.
إليك كيف يمكن تعديل مثالنا ليتحقق من توقيع قادم من GitHub كمثال:
const crypto = require('crypto');
const express = require('express');
const app = express();
// استخدم `express.raw` بدلاً من `express.json` لتتمكن من حساب التوقيع على الجسم الخام للطلب
app.use(express.raw({ type: 'application/json' }));
const GITHUB_SECRET = 'my_super_secret_key_from_github';
app.post('/webhooks/github', (req, res) => {
// 1. احصل على التوقيع من الـ Headers
const signature = req.headers['x-hub-signature-256'];
if (!signature) {
return res.status(401).send('Signature required');
}
// 2. احسب التوقيع المتوقع من طرفك
const expectedSignature = `sha256=${crypto
.createHmac('sha256', GITHUB_SECRET)
.update(req.body) // استخدم الجسم الخام (raw body) هنا
.digest('hex')}`;
// 3. قارن بين التوقيعين بشكل آمن
const isSignatureValid = crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
if (!isSignatureValid) {
return res.status(401).send('Invalid signature');
}
// الآن فقط يمكننا الوثوق بالبيانات ومعالجتها
const payload = JSON.parse(req.body);
console.log('GitHub event received and verified:', payload.action);
res.status(200).send('Received and verified');
});
نصيحة عملية: لا تقم أبداً بمقارنة التواقيع باستخدام == أو === لأنها عرضة لهجمات التوقيت (Timing Attacks). استخدم دائماً دالة مخصصة للمقارنة الآمنة مثل crypto.timingSafeEqual.
متى نستخدم الـ Polling ومتى نلجأ للـ Webhooks؟
رغم تفوق الـ Webhooks، إلا أن الـ Polling له مكانه. إليك مقارنة سريعة:
- استخدم الـ Webhooks (الخيار الأفضل) عندما:
- تحتاج إلى تحديثات في الوقت الفعلي (Real-time).
- تريد بناء نظام فعال وموجه بالأحداث.
- الأحداث غير متوقعة أو نادرة الحدوث.
- الخدمة الخارجية تدعم الـ Webhooks (وهو الحال مع معظم الخدمات الحديثة).
- استخدم الـ Polling (كملاذ أخير) عندما:
- الخدمة الخارجية لا تدعم الـ Webhooks على الإطلاق. (هذا هو السبب الأكثر شيوعاً).
- البيانات تتغير بشكل مستمر وبمعدل ثابت جداً (حالة نادرة).
- تحتاج إلى سحب البيانات في أوقات محددة تماماً من طرفك ولا يهمك التأخير.
خلاصة الحكي ونصيحة من أبو عمر
الانتقال من عقلية “الاستجداء” (Polling) إلى عقلية “الاستقبال” (Webhooks) هو نقلة نوعية في طريقة تفكيرك كمطور. إنه يحرر موارد خادمك، يقلل من تعقيد الكود، ويجعل تطبيقاتك أسرع وأكثر استجابة بشكل جذري. في المرة القادمة التي تحتاج فيها للتكامل مع خدمة خارجية، أول سؤال يجب أن تطرحه هو: “هل تدعمون الـ Webhooks؟”.
تذكر دائماً، في عالم البرمجة الحديث، الكفاءة هي الملك. لا تكن أنت الطفل الذي يسأل كل دقيقة “هل وصلنا؟”، بل كن الشخص الذكي الذي يضبط المنبه وينتظر بهدوء الإشعار عند الوصول. 😉