شرح Webhooks: الحمولات الموقعة وإعادة المحاولة الموثوقة
تقوم بنشر تكامل الدفع، ويطلق المزود webhook، ويُرجع نقطة النهاية 200 — ولكن بعد ثلاث ساعات يبلغ الدعم عن طلبات مفقودة. أو الأسوأ: يصل نفس الحدث أربع مرات ويُخصم من العميل مرتين. تبدو webhooks بسيطة (طلب HTTP POST إلى عنوان URL الخاص بك) لكن الأجزاء الصعبة هي الثقة وضمانات التسليم.
يغطي هذا الدليل كليهما: كيفية التحقق من أن الحمولة جاءت فعلاً من المرسل باستخدام الحمولات الموقعة، وكيفية بناء نموذج إعادة محاولة يتحمل التوقف والمهلات والتسليمات المكررة.
ما هو webhook فعليًا
webhook هو استدعاء API عكسي. بدلاً من أن يستقصي خادمك GET /events كل دقيقة، يدفع المزود طلب HTTP إلى عنوان URL قمت بتسجيله عند حدوث شيء ما. تكون الحمولة عادة JSON، والطريقة دائمًا تقريبًا POST، والاستجابة التي تُرجعها هي إشارة: 2xx تعني "تم الاستلام"، وأي شيء آخر يعني "حاول مرة أخرى لاحقًا".
تلك الجملة الأخيرة هي العقد بأكمله. كل شيء آخر — التوقيعات، إعادة المحاولة، الترتيب — موجود لجعل هذا العقد آمنًا عبر شبكة غير موثوقة.
لماذا تعتبر webhooks غير الموقعة خطرة
نقطة نهاية webhook الخاصة بك هي عنوان URL عام. أي شخص يكتشفه يمكنه إرسال POST بحمولة مزيفة تدعي أن طلبًا قد تم دفعه. إذا كنت تثق بالجسم دون التحقق، فقد بنيت بابًا مفتوحًا لمنطق الفوترة الخاص بك.
تساعد قوائم IP المسموح بها قليلاً لكنها تنكسر خلف CDNs، والخروج من الخادم بدون خادم، وتغييرات IP المزود. الإجابة القوية هي مفتاح سري مشترك بالإضافة إلى توقيع HMAC على جسم الطلب الخام.
كيف تعمل الحمولات الموقعة
تشارك أنت والمزود مفتاحًا سريًا. في كل تسليم يحسب المزود HMAC (عادة SHA-256) على الجسم الخام — وأحيانًا على طابع زمني بالإضافة إلى الجسم — ويرسل الملخص السداسي في رأس مثل X-Signature. أنت تعيد حساب نفس الملخص بنسختك من السر وتقارن.
هناك تفصيلان يهمان أكثر مما يتوقع الناس:
- وقّع البايتات الخام. إذا قام إطار العمل الخاص بك بتحليل JSON وإعادة تسلسله، يتغير ترتيب المفاتيح والمسافات البيضاء ولن يتطابق الملخص. اقرأ الجسم الخام أولاً.
- قارن في وقت ثابت.
==الساذج يسرب معلومات التوقيت. استخدم دالة مقارنة ثابتة الوقت من مكتبة التشفير في لغتك.
مثال تحقق بسيط (Node.js)
const crypto = require('crypto');
function verifySignature(rawBody, headerSig, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody) // raw Buffer or string, not parsed JSON
.digest('hex');
const a = Buffer.from(expected, 'utf8');
const b = Buffer.from(headerSig || '', 'utf8');
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}
نفس النمط يعمل في Python مع hmac.compare_digest، وفي Go مع hmac.Equal، وفي Java مع MessageDigest.isEqual. المبدأ متطابق في كل مكان: HMAC على البايتات الخام، مقارنة ثابتة الوقت.
حماية إعادة التشغيل باستخدام الطوابع الزمنية
التوقيع الصالح يثبت أن الحمولة جاءت من شخص يحمل السر — لكن الطلب الملتقط يمكن إعادة تشغيله إلى الأبد. المزودون الذين يهتمون بهذا يتضمنون طابعًا زمنيًا في السلسلة الموقعة، على سبيل المثال t=1712345678,v1=<hex>. من جانبك:
- استخرج الطابع الزمني من الرأس.
- ارفض الطلب إذا كان أقدم من نافذة السماحية (غالبًا 5 دقائق).
- أعد حساب HMAC على
timestamp + "." + rawBody.
هذا يحد من الضرر الناجم عن طلب مسرب إلى بضع دقائق بدلاً من الأبد.
تصميم إعادة المحاولة التي تسلم فعلاً
تفشل الشبكات، وتعيد خدمتك التشغيل، وتنشر القرون. المزود الجاد يعيد محاولة التسليمات الفاشلة وفق جدول. المستقبل الجاد يجب أن يفترض أن كل حدث يمكن أن يصل أكثر من مرة.
قواعد إعادة المحاولة من جانب المرسل
- تعامل مع أي استجابة غير 2xx، أو خطأ اتصال، أو مهلة كفشل.
- أعد المحاولة مع تراجع أسي وتذبذب — على سبيل المثال 1s، 5s، 30s، 5m، 30m، 2h — حتى لا ينتج انقطاع المزود قطيعًا مدويًا عند التعافي.
- حدد عدد المحاولات (عادة 5–10) وسجل الفشل النهائي حتى يتمكن المستخدم أو المشغل من رؤيته.
- اضبط مهلة عميل قصيرة (5–10 ثوانٍ). يجب أن يقر معالج webhook بسرعة وينفذ العمل بشكل غير متزامن.
قواعد جانب المستقبل
- تحقق أولاً، حلل ثانيًا. ارفض التوقيعات السيئة باستخدام 401 قبل لمس قاعدة البيانات.
- احفظ الحدث، ثم أعد 200. اكتب الحمولة الخام ومعرف الحدث في قائمة انتظار أو جدول، واستجب فورًا، وعالج في عامل. إذا فشلت المعالجة، أعد المحاولة داخليًا.
- اجعل المعالجات خاملة. استخدم معرف الحدث الخاص بالمزود كمفتاح فريد. إذا كان موجودًا بالفعل، أعد 200 دون إعادة تنفيذ الآثار الجانبية.
- أعد الأخطاء بصدق. إذا كانت قاعدة البيانات معطلة، أعد 500 حتى يعيد المزود المحاولة — لا تعد 200 وتتجاهل الحدث بصمت.
| الاستجابة | المعنى للمرسل | ما يحدث بعد ذلك |
|---|---|---|
| 200 / 204 | تم التسليم | لا إعادة محاولة |
| 400 | مشوه أو مرفوض | عادة لا إعادة محاولة؛ سجل لتصحيح الأخطاء |
| 401 | توقيع غير صالح | لا إعادة محاولة؛ تحقق من السر |
| 429 | محدود المعدل | أعد المحاولة مع التراجع |
| 5xx / مهلة | فشل مؤقت | أعد المحاولة مع التراجع |
الخمول في الممارسة
الخمول ليس مجرد كلمة رنانة هنا — إنه الشيء الوحيد الذي يقف بين إعادة المحاولة والخصم المزدوج. النمط:
INSERT INTO webhook_events (event_id, payload, received_at)
VALUES ($1, $2, now())
ON CONFLICT (event_id) DO NOTHING;
إذا أثر الإدراج على صفر صفوف، فقد رأيت هذا الحدث بالفعل؛ أقر وامضِ قدمًا. هذا يعمل حتى في ظل التسليمات المتزامنة لأن القيد الفريد يسلسلها في قاعدة البيانات.
بالنسبة للآثار الجانبية التي يجب أن تحدث مرة واحدة بالضبط (إرسال بريد إلكتروني، خصم بطاقة)، مرر مفتاح الخمول الخاص بك إلى المصب — معظم واجهات برمجة التطبيقات للدفع والبريد الإلكتروني تقبل واحدًا.
الترتيب، تصحيح الأخطاء، والمراقبة
لا تفترض أن الأحداث تصل بالترتيب. يمكن أن تعيد المحاولات ترتيب التسليمات، وكذلك العمال المتوازيون. إذا كان الترتيب مهمًا، فضمّن رقم تسلسل أو طابع زمني في الحمولة وقم بتسوية الحالة بدلاً من تطبيق الفروق بشكل أعمى.
سجل كل محاولة تسليم مع معرف الحدث، نتيجة التوقيع، رمز الاستجابة، ووقت المعالجة. عندما يقول العميل "لم يصل طلبي أبدًا"، فأنت تريد الإجابة في ثوانٍ: هل تم استلام webhook، هل كان التوقيع صالحًا، هل نجحت المعالجة؟ سطر سجل منظم لكل حدث يكفي.
بالنسبة لنقاط النهاية التي تتلقى الكثير من الحركة، راقب عواصف إعادة المحاولة بعد النشر. طريقة سريعة لاكتشافها هي النظر إلى أنماط الطلبات في سجلات الوصول الخاصة بك — ارتفاع مفاجئ لمسارات متطابقة من IP واحد مع استجابات 5xx هو حلقة إعادة محاولة كلاسيكية. أدوات مثل محلل سجلات Nginx تجعل هذا النمط واضحًا دون كتابة استعلام يدويًا.
الأخطاء الشائعة التي يجب تجنبها
- تحليل JSON قبل التحقق من التوقيع.
- استخدام نفس السر عبر البيئات، أو عدم تدويره أبدًا.
- القيام بعمل شاق (إنشاء PDF، استدعاءات طرف ثالث) داخل معالج الطلب.
- إرجاع 200 عند أخطاء المعالجة، حتى لا يعيد المزود المحاولة أبدًا.
- الثقة بمبلغ الحمولة أو حالتها دون إعادة جلب المورد من API المزود عندما يكون ذلك مهمًا.
الأسئلة الشائعة
كيف أتحقق من توقيع webhook إذا كان الجسم JSON؟
اقرأ جسم الطلب الخام كبايتات قبل أي تحليل JSON، واحسب HMAC على تلك البايتات، وقارن مع الرأس باستخدام دالة ثابتة الوقت. JSON المعاد تسلسله سينتج ملخصًا مختلفًا.
كم مرة يجب إعادة محاولة webhook؟
لا يوجد رقم عالمي، لكن 5–10 محاولات على مدى عدة ساعات مع تراجع أسي وتذبذب شائع. الجزء المهم هو أن المستقبلات خاملة، لذا فإن إعادة المحاولة آمنة حتى عندما تكون متكررة.
ما رمز الحالة الذي يجب أن ترجعه نقطة نهاية webhook الخاصة بي؟
أعد 2xx فقط بعد تخزين الحدث بشكل دائم. أعد 401 للتوقيعات غير الصالحة، و400 للحمولات المشوهة، و5xx أو 429 عندما تريد من المرسل إعادة المحاولة.
إذا كنت تختبر حمولات webhook أو تسجل JSON الذي تتلقاه، فإن منسق JSON هو طريقة سريعة لطباعة حدث ملتقط والتحقق من صحته قبل بناء منطق التحليل حوله.