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

Ashley Innocent

Ashley Innocent

20 مارس 2026

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

Apidog للمؤسسات

نشر محلي

SSO & RBAC

متوافق مع SOC 2

استكشاف Apidog Enterprise

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

فهم الفرق

الاستقصاء (Polling): يطلب العميل "هل من تحديثات؟" بشكل متكرر. الـ 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);

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

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

GET /api/v1/orders/123
# Returns current order state

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

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

# Returns 304 Not Modified if unchanged
# Returns 200 with new data if changed

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

GET /api/v1/orders/123/events?since=1710331200
# Returns events since timestamp

الـ 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

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

جيد لـ:

أمثلة:

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

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

جيد لـ:

أمثلة:

الـ 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}`);
});

الاستقصاء الذكي (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
  }
);

الاستقصاء باستخدام 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();
}

تنفيذ الـ 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');
}

استقبال الـ 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)
  );
}

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

# Use ngrok to expose local endpoint
ngrok http 3000

# Register ngrok URL as webhook endpoint
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"]
  }'

تسليم الـ 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}`);
  }
}

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

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

النهج الهجين

استخدم كلاً من الاستقصاء والـ 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'
      });
    }
  }
}

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

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

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

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

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

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

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

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

ممارسة تصميم API في Apidog

اكتشف طريقة أسهل لبناء واستخدام واجهات برمجة التطبيقات