باختصار: الاستقصاء (Polling) يستعلم عن التحديثات بشكل دوري — بسيط لكنه غير فعال. الـ Webhooks تدفع التحديثات في الوقت الفعلي — أكثر فاعلية لكنها تتطلب إعداداً إضافياً. استخدم الاستقصاء للفحوصات غير المتكررة، والـ Webhooks للتحديثات الفورية. يدعم Modern PetstoreAPI كلا النمطين مع تسليم موثوق للـ Webhooks.
فهم الفرق
الاستقصاء (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);
أنماط الاستقصاء:
استقصاء بسيط:
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
مناسب لـ:
- التحديثات في الوقت الفعلي
- مراقبة عدة موارد
- الأحداث الحساسة للوقت
- الدمج مع أطراف ثالثة
- تحديثات عالية التردد
أمثلة عملية:
- تأكيدات الدفع
- رسائل الدردشة
- تنبيهات أسعار الأسهم
- تغييرات حالة الطلب
- إشعارات 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}`);
});
الاستقصاء الذكي (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 محليًا
# استخدم 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"]
}'
تسليم الـ 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 للأحداث الجديدة فقط. استخدم الاستقصاء أو API مجمعة للبيانات التاريخية.
س: هل يجب أن أستخدم الاستقصاء أم الـ webhooks لتطبيقات الجوال؟ الاستقصاء أبسط لتطبيقات الجوال. الـ webhooks تتطلب إشعارات دفع كوسيط.
س: كيف أصحح مشاكل الـ webhook؟ استخدم أدوات مثل webhook.site للاختبار. سجّل كل عمليات تسليم الـ webhook. وفر سجل أحداث webhook في API الخاصة بك.
يدعم Modern PetstoreAPI كلاً من الاستقصاء والـ webhooks. راجع دليل الـ webhooks للحصول على تفاصيل التنفيذ. اختبر تكامل الـ webhook باستخدام Apidog.
Top comments (0)