تصميم Webhooks تتحمل ظروف التشغيل الحقيقية
بنية عملية لاستقبال أحداث Webhook والتحقق منها ومعالجتها وإعادة تشغيلها بدون فقدان البيانات.
نقطة استقبال Webhook يجب أن تؤكد الطلب بسرعة، وتحفظ الحدث الأصلي، ثم تنقل العمل المعرض للفشل إلى مهمة خلفية قابلة لإعادة التشغيل بأمان.
تبدو Webhooks بسيطة في الرسم المعماري: نظام يرسل طلب HTTP ونظام آخر يعالجه. لكن بيئة التشغيل تضيف الحالات الصعبة مثل تكرار الحدث، وفشل التحقق، وانتهاء المهلة، ووصول الأحداث بترتيب مختلف، وتعطل خدمة يعتمد عليها النظام.
التصميم الموثوق ليس Controller يحتوي على كل شيء، بل بوابة استقبال صغيرة يتبعها مسار معالجة دائم وواضح.
اجعل نقطة الاستقبال محدودة المسؤولية
للطلب العام ثلاث مهام فقط:
- قراءة جسم الطلب كما وصل.
- التحقق من أن المزود هو من أرسله.
- حفظ الحدث قبل إرسال استجابة ناجحة.
تحديث الطلبات وإرسال البريد وإنشاء الفواتير والاتصال بخدمات أخرى يجب أن يتم خارج دورة الطلب. كل خطوة إضافية تؤخر التأكيد وتزيد احتمال أن يعيد المزود إرسال الحدث.
public function __invoke(Request $request): Response
{
$payload = $request->getContent();
$this->signatures->verify(
payload: $payload,
signature: $request->header('Webhook-Signature')
);
$event = WebhookEvent::firstOrCreate(
['provider_id' => $request->header('Webhook-Id')],
['payload' => $payload, 'status' => 'received']
);
ProcessWebhook::dispatch($event->id);
return response()->noContent();
}
تحقق من البايتات الأصلية
يحسب كثير من المزودين التوقيع من النص الأصلي للطلب. تحويل JSON ثم إعادة تكوينه قد يغير ترتيب المفاتيح أو المسافات. تحقق من الجسم الخام أولاً، ثم حلله.
استخدم مقارنة ثابتة الزمن للتوقيع، وافحص توقيت الحدث إذا كان البروتوكول يرسله لتقليل إعادة استخدام الطلبات القديمة. ولا تكتب الأسرار أو رؤوس التفويض في السجلات.
اجعل المعالجة Idempotent
التسليم المتكرر سلوك طبيعي لدى أغلب المزودين. لذلك يجب أن يؤدي الحدث نفسه إلى النتيجة النهائية نفسها مهما تكرر.
ضع قيداً فريداً على معرف الحدث القادم من المزود، وأضف حماية داخل منطق العمل نفسه. إنشاء اشتراك مثلاً يجب أن يعتمد على معرف الاشتراك الخارجي، وليس معرف مهمة الـ Queue.
إعادة محاولة المهمة ليست حالة استثنائية، بل مسار طبيعي يجب تصميمه واختباره.
افصل الاستقبال عن المعالجة والنتيجة
سجل الحدث الجيد يحتوي على معرف المزود، ونوع الحدث، والنص الأصلي، ووقت الاستقبال، وحالة المعالجة، وعدد المحاولات، وآخر خطأ. هذه البيانات تجعل التحقيق وإعادة التشغيل ممكنين.
احتفظ بالحمولة الأصلية لمدة محددة، واحذف البيانات الحساسة التي لا تحتاجها، وقيّد الوصول إلى مخزن الأحداث.
صمم المحاولات بعناية
أعد المحاولة عند فشل مؤقت مثل timeout أو انقطاع الاتصال أو rate limit. لا تكرر إلى ما لا نهاية عند حمولة غير صالحة أو انتقال حالة مستحيل.
استخدم تراجعاً زمنياً تصاعدياً مع jitter حتى لا تضرب آلاف المهام الخدمة المتعافية في اللحظة نفسها. وبعد آخر محاولة، انقل الحدث إلى حالة فشل قابلة للمراجعة والتنبيه.
اختبر مسارات الفشل
اختبر التوقيع الخاطئ، والحدث المكرر، والأحداث بترتيب مختلف، وفشل العامل بعد أول كتابة في قاعدة البيانات، ونجاح الخدمة بعد timeout، وتدوير الأسرار، وإعادة تشغيل حدث مكتمل.
البنية الجيدة تكلف أكثر قليلاً من وضع المنطق كله في Controller، لكنها أرخص كثيراً من محاولة إعادة بناء مدفوعات أو بيانات مفقودة من سجلات ناقصة.
نهاية الملاحظة.