باختصار: يستعلم الاستقصاء (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 محليًا
# 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.
