المشكلة: "هل تغيّر شيء؟"
تخيّل تطبيقك محتاج يعرف فورًا لما عملية دفع تنجح عند مزوّد خارجي (Stripe مثلًا). الحل الساذج: تسأله كل شوي.
كل 10 ثواني: GET /payments/123/status
لو عندك 1000 عملية نشطة، هذا 6000 طلب بالدقيقة — معظمها بيرجّع "لسا ما تغيّر شي". هذا اسمه Polling (استطلاع): مضيعة موارد، بياكل حصتك من الـ rate limit عند المزوّد، وبرضه فيه تأخير لحد الاستطلاع التالي.
الحل: Webhooks
Webhook هو عكس REST المعتاد بالاتجاه: بدل ما عميلك يسأل الخادم، الخادم (المزوّد) هو يلي بيرسل طلب POST لرابط حددته أنت، لحظة ما يصير حدث معيّن. بمصطلح تاني: REST عادي "سحب" (pull) — أنت بتطلب البيانات. Webhook "دفع" (push) — البيانات بتوصلك أول ما تصير.
بدل: أنت تسأل "هل الدفع نجح؟" كل 10 ثواني
↓
Webhook: المزوّد يرسل POST /webhooks/payment لحظة نجاح الدفع فعليًا
هذا النمط أساس أنظمة كبيرة كتير — Stripe وGitHub وShopify وSlack كلها بتعتمد Webhooks كآلية الإشعار الأساسية بدل إجبار عملائها على الاستطلاع.
استقبال Webhook — نقطة نهاية عادية
من ناحية الخادم عندك، الـ webhook مجرد مسار POST عادي بانتظار طلبات من طرف خارجي بدل مستخدمك:
app.post("/webhooks/payment", express.json(), (req, res) => {
const event = req.body;
if (event.type === "payment.succeeded") {
// حدّث حالة الطلب بقاعدة البيانات
}
res.status(200).json({ received: true }); // أخبر المُرسِل إنك استلمت
});
الفرق الجوهري عن أي مسار عادي: المُرسِل مو مستخدمك المصادَق عليه بجلسة أو JWT — هو خادم خارجي. هذا يفتح سؤال أمان مهم: كيف تتأكد إن الطلب فعلًا من المزوّد يلي تتوقعه، مو من أي شخص عرف رابطك وأرسل POST مزيّف؟
الأمان — توقيع HMAC
المعيار المتّبع (تستخدمه GitHub وStripe وأغلب المزوّدين الكبار): المُرسِل يوقّع الجسم بمفتاح سري مشترك (Signing Secret) عبر HMAC-SHA256، ويرفق التوقيع بترويسة مخصَّصة. خادمك يعيد حساب نفس التوقيع محليًا ويقارنه.
| المزوّد | الترويسة | الخوارزمية |
|---|---|---|
| GitHub | X-Hub-Signature-256 | HMAC-SHA256 |
| Stripe | Stripe-Signature | HMAC-SHA256 (مع طابع زمني ضمن التوقيع) |
import crypto from "node:crypto";
function verifySignature(payload, signatureHeader, secret) {
const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex");
// مقارنة بزمن ثابت (constant-time) — تمنع هجمات القياس الزمني (timing attacks)
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signatureHeader)
);
}
app.post(
"/webhooks/github",
express.raw({ type: "application/json" }), // الجسم الخام مطلوب للتوقيع، لا JSON محلَّل
(req, res) => {
const signature = req.headers["x-hub-signature-256"];
if (!verifySignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).json({ error: "توقيع غير صالح" });
}
// آمن الآن — تابع معالجة الحدث
res.status(200).json({ received: true });
}
);
⚠️ لازم تقارن التوقيعين بدالة "مقارنة بزمن ثابت" متل
crypto.timingSafeEqualلا عبر===العادي — المقارنة العادية بتكشف معلومات عن التوقيع الصحيح حرفًا حرفًا عبر فرق التوقيت الدقيق، وهذا بالضبط ما يستغله هجوم "timing attack".
أفضل الممارسات
- رد بـ 200 بسرعة، عالج لاحقًا. المزوّد بينتظر ردًا خلال مهلة قصيرة؛ لو منطق المعالجة بطيء (إرسال بريد، تحديث معقّد)، ضع الحدث بطابور (queue) ورد فورًا، عالجه بشكل غير متزامن.
- تعامل مع التكرار. أغلب المزوّدين بيعيدون إرسال نفس الحدث لو ما استلموا 200 بالوقت المحدد (Stripe مثلًا بيعيد المحاولة لعدة أيام). خزّن معرّف كل حدث معالَج (
event.id) وتجاهل التكرار. - تحقق من التوقيع دائمًا. بدون توقيع HMAC، أي شخص عرف رابط الـ webhook فيك يقدر يرسل أحداث مزيّفة.
- لا تثق بمحتوى الحدث فقط لأنه "من مزوّد موثوق". تحقق من التوقيع أولًا، بعدين عالج — مو العكس.
خلاصة
| REST عادي (Polling) | Webhook | |
|---|---|---|
| الاتجاه | أنت تسأل الخادم | الخادم يرسل لك |
| التأخير | يعتمد على تكرار الاستطلاع | فوري تقريبًا |
| استهلاك الموارد | مرتفع (طلبات متكررة بلا فائدة غالبًا) | منخفض (طلب واحد عند وقوع الحدث فعليًا) |
| الأمان | مصادقة عادية (JWT/API Key) | توقيع HMAC للتحقق من هوية المُرسِل |
Webhooks مش بديل عن REST APIs — هي إضافة له. غالبًا بتستخدم الاثنين معًا: الـ webhook يخبرك إن حدث وقع، وطلب REST عادي بعده يجيب لك التفاصيل الكاملة لو احتجتها.
🎯 العودة إلى قائمة دروس REST API