DEV Community

Cover image for الـ Webhooks ضد الـ Polling: أي نمط تكامل API هو الأفضل؟
Yusuf Khalidd
Yusuf Khalidd

Posted on • Originally published at apidog.com

الـ Webhooks ضد الـ Polling: أي نمط تكامل API هو الأفضل؟

باختصار: الاستقصاء (Polling) يستعلم عن التحديثات بشكل دوري — بسيط لكنه غير فعال. الـ Webhooks تدفع التحديثات في الوقت الفعلي — أكثر فاعلية لكنها تتطلب إعداداً إضافياً. استخدم الاستقصاء للفحوصات غير المتكررة، والـ Webhooks للتحديثات الفورية. يدعم Modern PetstoreAPI كلا النمطين مع تسليم موثوق للـ Webhooks.

جرّب Apidog اليوم

فهم الفرق

الاستقصاء (Polling): العميل يطلب بشكل متكرر "هل من تحديثات؟". الـ Webhooks: الخادم يرسل "إليك تحديث!" عند حدوث حدث.

  • الاستقصاء = التحقق من صندوق بريدك كل ساعة
  • الـ Webhooks = ساعي البريد يدق جرس الباب عند وصول البريد

الاستقصاء: كيف يعمل

يرسل العميل طلبات دورية للتحقق من التغييرات:

// Poll every 30 seconds
setInterval(async () => {
  const response = await fetch('https://petstoreapi.com/api/v1/orders/123');
  const order = await response.json();

  if (order.status === 'completed') {
    console.log('Order completed!', order);
    clearInterval(pollInterval);
  }
}, 30000);
Enter fullscreen mode Exit fullscreen mode

أنماط الاستقصاء:

استقصاء بسيط:

GET /api/v1/orders/123
# Returns current order state
Enter fullscreen mode Exit fullscreen mode

استقصاء شرطي (ETag):

GET /api/v1/orders/123
If-None-Match: "abc123"

# Returns 304 Not Modified if unchanged
# Returns 200 with new data if changed
Enter fullscreen mode Exit fullscreen mode

استقصاء يعتمد على "منذ" (Since-based polling):

GET /api/v1/orders/123/events?since=1710331200
# Returns events since timestamp
Enter fullscreen mode Exit fullscreen mode

الـ Webhooks: كيف تعمل

الخادم يرسل طلب HTTP POST إلى نقطة النهاية الخاصة بك عند حدوث حدث:

// 1. Register webhook endpoint
POST /api/v1/webhooks
{
  "url": "https://myapp.com/webhooks/petstore",
  "events": ["order.created", "order.completed"],
  "secret": "whsec_abc123"
}

// 2. Server sends webhook when event occurs
POST https://myapp.com/webhooks/petstore
{
  "id": "evt_123",
  "type": "order.completed",
  "created": 1710331200,
  "data": {
    "orderId": "123",
    "status": "completed",
    "completedAt": "2024-01-01T12:00:00Z"
  }
}

// 3. Verify and process webhook
// Respond with 200 OK
Enter fullscreen mode Exit fullscreen mode

متى تستخدم الاستقصاء

مناسب لـ:

  • الفحوصات غير المتكررة (مرة في الساعة)
  • عدد قليل من الموارد
  • تطبيقات بسيطة
  • عندما تتحكم في العميل
  • الاختبار وتصحيح الأخطاء

أمثلة عملية:

  • التحقق من حالة التقرير اليومي
  • مزامنة جهات الاتصال
  • مراقبة صحة الخادم
  • التحقق من حالة الدفع غير المتكررة

الاستقصاء مثالي عندما:

  • التحديثات نادرة
  • يمكن قبول بعض التأخير
  • تريد تطبيقاً بسيطاً
  • المورد صغير

متى تستخدم الـ Webhooks

مناسب لـ:

  • التحديثات في الوقت الفعلي
  • مراقبة عدة موارد
  • الأحداث الحساسة للوقت
  • الدمج مع أطراف ثالثة
  • تحديثات عالية التردد

أمثلة عملية:

  • تأكيدات الدفع
  • رسائل الدردشة
  • تنبيهات أسعار الأسهم
  • تغييرات حالة الطلب
  • إشعارات CI/CD

الـ Webhooks أفضل عندما:

  • يجب أن تكون التحديثات فورية
  • الاستقصاء غير فعال
  • عدة عملاء يراقبون نفس المورد
  • تريد تقليل الحمل على الخادم

جدول المقارنة

العامل الاستقصاء (Polling) الـ Webhooks
الكمون حتى فترة الاستقصاء في الوقت الفعلي
حمل الخادم مرتفع (العديد من الطلبات الفارغة) منخفض (الأحداث الحقيقية فقط)
التعقيد بسيط معقد
الموثوقية عالية (العميل يتحكم في إعادة المحاولة) متوسطة (تحتاج إلى منطق إعادة المحاولة)
الإعداد لا شيء تسجيل نقطة النهاية
مشاكل جدار الحماية لا شيء (صادر فقط) قد تحتاج إلى الإذن بالوصول (whitelisting)
التكلفة أعلى (المزيد من الطلبات) أقل (عدد أقل من الطلبات)
الأفضل لـ الفحوصات غير المتكررة التحديثات في الوقت الفعلي

تنفيذ الاستقصاء

الاستقصاء الأساسي

async function pollOrderStatus(orderId, callback) {
  let lastStatus = null;

  const poll = async () => {
    try {
      const response = await fetch(`https://petstoreapi.com/api/v1/orders/${orderId}`);
      const order = await response.json();

      // Only callback if status changed
      if (order.status !== lastStatus) {
        lastStatus = order.status;
        callback(order);
      }

      // Stop polling if terminal state
      if (['completed', 'cancelled'].includes(order.status)) {
        return;
      }

      // Continue polling
      setTimeout(poll, 5000);
    } catch (error) {
      console.error('Polling error:', error);
      setTimeout(poll, 30000); // Back off on error
    }
  };

  poll();
}

// Usage
pollOrderStatus('order-123', (order) => {
  console.log(`Order status: ${order.status}`);
});
Enter fullscreen mode Exit fullscreen mode

الاستقصاء الذكي (Exponential Backoff)

async function smartPoll(url, callback, options = {}) {
  const {
    maxRetries = 10,
    initialInterval = 1000,
    maxInterval = 60000,
    stopCondition = () => false
  } = options;

  let retries = 0;
  let interval = initialInterval;
  let lastData = null;

  const poll = async () => {
    try {
      const response = await fetch(url);
      const data = await response.json();

      // Callback if data changed
      if (JSON.stringify(data) !== JSON.stringify(lastData)) {
        lastData = data;
        callback(data);
      }

      // Stop if condition met
      if (stopCondition(data)) {
        return;
      }

      // Reset interval on successful request
      interval = initialInterval;

    } catch (error) {
      retries++;
      if (retries >= maxRetries) {
        throw new Error('Max retries exceeded');
      }
    }

    // Schedule next poll with exponential backoff
    setTimeout(poll, interval);
    interval = Math.min(interval * 2, maxInterval);
  };

  poll();
}

// Usage: Poll order until completed
smartPoll('https://petstoreapi.com/api/v1/orders/123',
  (order) => console.log('Order:', order),
  {
    stopCondition: (order) => ['completed', 'cancelled'].includes(order.status),
    initialInterval: 2000,
    maxInterval: 30000
  }
);
Enter fullscreen mode Exit fullscreen mode

الاستقصاء باستخدام ETag

async function pollWithEtag(url, callback) {
  let etag = null;

  const poll = async () => {
    const headers = {};
    if (etag) {
      headers['If-None-Match'] = etag;
    }

    const response = await fetch(url, { headers });

    if (response.status === 304) {
      // Not modified, continue polling
      setTimeout(poll, 30000);
      return;
    }

    const data = await response.json();
    etag = response.headers.get('etag');

    callback(data);
    setTimeout(poll, 30000);
  };

  poll();
}
Enter fullscreen mode Exit fullscreen mode

تنفيذ الـ Webhooks

تسجيل الـ Webhooks

// Register webhook endpoint
async function registerWebhook(url, events) {
  const response = await fetch('https://petstoreapi.com/api/v1/webhooks', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${API_KEY}`
    },
    body: JSON.stringify({
      url,
      events,
      secret: generateSecret()
    })
  });

  return response.json();
}

function generateSecret() {
  return 'whsec_' + crypto.randomBytes(32).toString('hex');
}
Enter fullscreen mode Exit fullscreen mode

استقبال الـ Webhooks

const express = require('express');
const crypto = require('crypto');
const app = express();

// Raw body parser for signature verification
app.use('/webhooks', express.raw({ type: 'application/json' }));

app.post('/webhooks/petstore', async (req, res) => {
  const signature = req.headers['x-petstore-signature'];
  const body = req.body;

  // Verify signature
  const isValid = verifySignature(body, signature, process.env.WEBHOOK_SECRET);

  if (!isValid) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const event = JSON.parse(body.toString());

  // Process event
  switch (event.type) {
    case 'order.created':
      await handleOrderCreated(event.data);
      break;
    case 'order.completed':
      await handleOrderCompleted(event.data);
      break;
    case 'order.cancelled':
      await handleOrderCancelled(event.data);
      break;
  }

  // Acknowledge receipt
  res.status(200).json({ received: true });
});

function verifySignature(payload, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}
Enter fullscreen mode Exit fullscreen mode

اختبار الـ Webhooks محليًا

# استخدم ngrok لفتح نقطة النهاية المحلية
ngrok http 3000

# سجل عنوان ngrok كنقطة نهاية webhook
curl -X POST https://petstoreapi.com/api/v1/webhooks \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "url": "https://abc123.ngrok.io/webhooks/petstore",
    "events": ["order.created", "order.completed"]
  }'
Enter fullscreen mode Exit fullscreen mode

تسليم الـ Webhook الموثوق

الـ Webhooks قد تفشل. نفّذ منطق إعادة المحاولة (retry logic) لضمان الاستلام.

جانب المرسل (الخادم)

// Queue webhooks for delivery
const webhookQueue = [];

async function sendWebhook(event) {
  const webhooks = await db.webhooks.findMany({
    where: { events: { contains: event.type } }
  });

  for (const webhook of webhooks) {
    webhookQueue.push({
      webhook,
      event,
      attempts: 0,
      nextAttempt: Date.now()
    });
  }

  processQueue();
}

async function processQueue() {
  const now = Date.now();

  for (const item of webhookQueue) {
    if (item.nextAttempt > now) continue;

    try {
      await deliverWebhook(item);
      // Remove from queue on success
      webhookQueue.splice(webhookQueue.indexOf(item), 1);
    } catch (error) {
      // Schedule retry with exponential backoff
      item.attempts++;
      item.nextAttempt = now + getBackoff(item.attempts);

      if (item.attempts >= 5) {
        // Mark as failed after 5 attempts
        await markWebhookFailed(item);
        webhookQueue.splice(webhookQueue.indexOf(item), 1);
      }
    }
  }

  setTimeout(processQueue, 5000);
}

function getBackoff(attempt) {
  // 1min, 5min, 15min, 1hr, 4hr
  const delays = [60000, 300000, 900000, 3600000, 14400000];
  return delays[attempt - 1] || delays[delays.length - 1];
}

async function deliverWebhook({ webhook, event }) {
  const signature = generateSignature(event, webhook.secret);

  const response = await fetch(webhook.url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Petstore-Signature': signature,
      'X-Petstore-Event': event.type
    },
    body: JSON.stringify(event),
    timeout: 10000
  });

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

جانب المستقبل (العميل)

// Idempotent webhook handling
const processedEvents = new Set();

app.post('/webhooks/petstore', async (req, res) => {
  const event = JSON.parse(req.body.toString());

  // Skip if already processed (idempotency)
  if (processedEvents.has(event.id)) {
    return res.status(200).json({ received: true });
  }

  try {
    await processEvent(event);
    processedEvents.add(event.id);

    // Clean up old event IDs (keep last 1000)
    if (processedEvents.size > 1000) {
      const arr = Array.from(processedEvents);
      arr.slice(0, arr.length - 1000).forEach(id => processedEvents.delete(id));
    }

    res.status(200).json({ received: true });
  } catch (error) {
    console.error('Webhook processing error:', error);
    // Return 5xx to trigger retry
    res.status(500).json({ error: 'Processing failed' });
  }
});

async function processEvent(event) {
  // Process the event
  switch (event.type) {
    case 'order.created':
      await handleOrderCreated(event.data);
      break;
    // ... handle other events
  }
}
Enter fullscreen mode Exit fullscreen mode

النهج الهجين

للحصول على أفضل موثوقية ووقت استجابة، استخدم الاستقصاء والـ Webhooks معًا.

class OrderMonitor {
  constructor(orderId, callback) {
    this.orderId = orderId;
    this.callback = callback;
    this.pollInterval = null;
  }

  async start() {
    // Start with polling for immediate feedback
    this.startPolling();

    // Register webhook for real-time update
    await this.registerWebhook();
  }

  startPolling() {
    this.pollInterval = setInterval(async () => {
      const order = await this.fetchOrder();
      this.callback(order);

      if (['completed', 'cancelled'].includes(order.status)) {
        this.stop();
      }
    }, 10000);
  }

  async registerWebhook() {
    const response = await fetch('https://petstoreapi.com/api/v1/webhooks', {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${TOKEN}` },
      body: JSON.stringify({
        url: 'https://myapp.com/webhooks/petstore',
        events: [`order.${this.orderId}`],
        oneTime: true // Auto-delete after first delivery
      })
    });

    this.webhookId = (await response.json()).id;
  }

  stop() {
    if (this.pollInterval) {
      clearInterval(this.pollInterval);
    }
    if (this.webhookId) {
      fetch(`https://petstoreapi.com/api/v1/webhooks/${this.webhookId}`, {
        method: 'DELETE'
      });
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

الأسئلة الشائعة

س: كم مرة يجب أن أقوم بالاستقصاء؟ يعتمد على مدى الإلحاح. 30 ثانية للوقت شبه الفعلي. 5 دقائق لغير العاجلة. وازن بين حداثة البيانات وحمل الخادم.

س: ماذا لو كانت نقطة نهاية الـ webhook الخاصة بي معطلة؟ مزودو الـ webhook الجيدون يعيدون المحاولة مع تراجع أسي. نفّذ التكرارية (idempotency) للتعامل مع عمليات التسليم المتكررة.

س: كيف أقوم بتأمين الـ webhooks؟ تحقق من التوقيعات باستخدام سر مشترك. استخدم HTTPS فقط. تحقق من صحة الحدث.

س: هل يمكنني استخدام الـ webhooks للبيانات التاريخية؟ لا. الـ webhooks للأحداث الجديدة فقط. استخدم الاستقصاء أو API مجمعة للبيانات التاريخية.

س: هل يجب أن أستخدم الاستقصاء أم الـ webhooks لتطبيقات الجوال؟ الاستقصاء أبسط لتطبيقات الجوال. الـ webhooks تتطلب إشعارات دفع كوسيط.

س: كيف أصحح مشاكل الـ webhook؟ استخدم أدوات مثل webhook.site للاختبار. سجّل كل عمليات تسليم الـ webhook. وفر سجل أحداث webhook في API الخاصة بك.

يدعم Modern PetstoreAPI كلاً من الاستقصاء والـ webhooks. راجع دليل الـ webhooks للحصول على تفاصيل التنفيذ. اختبر تكامل الـ webhook باستخدام Apidog.

Top comments (0)