اختبار Webhooks
Webhook استدعاء عكسي (Callback) عبر HTTP: عندما يقع حدث في نظام ما، وهو المزوّد (Provider)، يرسل طلب HTTP، يكون في الغالب POST بجسم JSON، إلى عنوان URL سجّله مسبقاً نظام آخر، وهو المستهلك (Consumer). مثال: عندما تنجح عملية دفع بالبطاقة، يرسل مزوّد الدفع الحدث payment.succeeded إلى https://shop.example/webhooks/payments، فيغيّر المتجر حالة الطلب 1042 إلى «مدفوع» ويرسل إيصالاً واحداً بالبريد.
ويفحص اختبار Webhooks طرفَي هذا الاستدعاء. فعلى المزوّد أن يرسل الحدث الموثّق موقّعاً، وأن يعيد المحاولة حين يفشل التسليم. وعلى المستهلك، وهو عادةً محور عمل المختبِر، أن يرفض الطلبات المزوّرة، ويُقرّ باستلام الحقيقية منها، ويجعل لكل حدث أثراً واحداً فقط، حتى لو وصل متأخراً أو مرتين أو بغير ترتيبه.
لماذا يهم
تعمل Webhooks دون أن يراقبها أحد. فإذا ضاع أحدها أو عولج مرتين، فلن تعرض أي شاشة خطأً في الغالب: يبقى الطلب المدفوع «قيد الانتظار»، أو يُخصم المخزون مرتين، أو يتلقى العميل إيصالين. وكثيراً ما يظهر العيب بعد أيام، حين يصعب إعادة إنتاجه.
ونقطة نهاية Webhook عنوان URL عام أيضاً. فمن يعرفه يستطيع إرسال حدث مزيّف بأن «الدفع نجح»، ولذلك فالتحقق من التوقيع ضابط أمني، ومن السهل تنفيذه بشكل خاطئ. واختباره يعني إرسال تسليمات مزوّرة ومعدّلة، وذلك فقط إلى أنظمة تملكها أو لديك إذن كتابي باختبارها.
الاستدعاءات العكسية والإقرار بالاستلام وإعادة المحاولة
في دروس REST كان العميل يسأل والخادم يجيب. أما Webhook فيعكس الأدوار: خادم المزوّد هو العميل، ونقطة نهاية المستهلك هي التي تستقبل الطلب. ويُقرّ المستهلك باستلام التسليم بأن يرد سريعاً برمز حالة 2xx؛ أما أي رمز آخر (3xx أو 4xx أو 5xx) أو انتهاء المهلة (Timeout) أو رفض الاتصال فيُعدّ في الغالب تسليماً فاشلاً.
ثم يعيد كثير من المزوّدين المحاولة بفواصل متزايدة بين المحاولات ولمدة محدودة؛ وبعضهم يعطّل نقطة النهاية التي يتكرر فشلها، وبعضهم لا يعيد المحاولة أصلاً ويتيح إعادة التسليم يدوياً بدلاً من ذلك. وتختلف قواعد الفشل والمهلة والجدول من مزوّد إلى آخر، لذا فتوثيق Webhooks لدى المزوّد هو أساس الاختبار (Test basis) لديك.
وإعادة المحاولة تعني أن التسليم يتم «مرة واحدة على الأقل» (At least once): فإذا أنجز المستهلك العمل لكن ردّه ضاع أو تأخر، يصل الحدث نفسه مجدداً. ولكل حدث في العادة معرّف (Event id) يبقى نفسه في كل إعادة محاولة (وبعض المزوّدين يضيفون معرّفاً منفصلاً لكل محاولة)، وبهذا المعرّف يُكتشف التكرار. والترتيب غير مضمون عادةً أيضاً: فقد يصل payment.succeeded المُعاد بعد payment.refunded الذي تلاه.
تساوي الأثر: لكل حدث أثر واحد
تكون المعالجة متساوية الأثر (Idempotent) عندما يكون لمعالجة الحدث مرتين الأثر نفسه لمعالجته مرة واحدة، كما في طرق HTTP المتكافئة. والتصميم المعتاد يخزّن معرّف الحدث في جدول عليه قيد تفرّد (Unique constraint)، وهو قاعدة في قاعدة البيانات ترفض صفاً ثانياً بالقيمة نفسها. فإذا كان المعرّف موجوداً، لا يفعل المستهلك شيئاً ويرد مع ذلك بـ 2xx؛ لأن رمز الخطأ لن يؤدي إلا إلى أن يعيد المزوّد محاولة حدث عولج من قبل. واسأل ماذا يحدث إذا خُزّن المعرّف ثم فشل العمل نفسه: يجب ألا تُتجاهَل إعادة المحاولة على أنها تكرار، وإلا ضاع الحدث.
وقد تصل نسختان من الحدث في اللحظة نفسها إلى خادمين يعملان بالتوازي. والنمط «افحص ثم أدخِل» في الشيفرة قد يمرّر النسختين؛ أما قيد التفرّد في قاعدة البيانات فيوقف الثانية. ولمواجهة اختلاف الترتيب، يقارن المستهلك الحدث بالحالة الحالية، فلا يعود طلب مُسترَد إلى «مدفوع» أبداً.
- مزوّد الدفع يرسل ويعيد المحاولة
- نقطة Webhook في المتجر يتحقق ويسجّل ويُقرّ بالاستلام
- قاعدة بيانات المتجر قيد تفرّد على معرّف الحدث
- 1 من مزوّد الدفع إلى نقطة Webhook في المتجر: المحاولة 1: إرسال الحدث الموقَّع
id: evt_7f3a91 - 2 نقطة Webhook في المتجر داخليًا: التحقق من التوقيع والطابع الزمني نجاح
- 3 من نقطة Webhook في المتجر إلى قاعدة بيانات المتجر: إدخال معرّف الحدث وتغيير حالة الطلب 1042 إلى PAID يُضاف إيصال واحد إلى قائمة الإرسال
- 4 من نقطة Webhook في المتجر إلى مزوّد الدفع: الرد ضاع أو وصل بعد انتهاء المهلة ردّ يسجّل المزوّد تسليماً فاشلاً فشل
- 5 مزوّد الدفع داخليًا: الانتظار ثم إعادة المحاولة فواصل متزايدة بين المحاولات تنبيه
- 6 من مزوّد الدفع إلى نقطة Webhook في المتجر: المحاولة 2: الحدث نفسه مرة أخرى معرّف الحدث نفسه بطابع زمني وتوقيع جديدين
id: evt_7f3a91 - 7 نقطة Webhook في المتجر داخليًا: التحقق من التوقيع والطابع الزمني نجاح
- 8 من نقطة Webhook في المتجر إلى قاعدة بيانات المتجر: إدخال معرّف الحدث نفسه
- 9 من قاعدة بيانات المتجر إلى نقطة Webhook في المتجر: موجود مسبقاً: قيد التفرّد يرفضه ردّ لا إيصال ثانياً ولا تغيير في الطلب
- 10 من نقطة Webhook في المتجر إلى مزوّد الدفع: 200 OK: إقرار باستلام الحدث المكرر ردّ نجاح
النتيجة: سُلِّم مرتين وعولج مرة واحدة: الطلب 1042 مدفوع، ويتلقى العميل إيصالاً واحداً فقط. نجاح
التحقق من التوقيع
يوقّع كثير من المزوّدين كل تسليم بـ HMAC (RFC 2104)، وهو تجزئة (Hash) تُحسب بمفتاح سرّي، فلا يستطيع إنتاج القيمة الصحيحة إلا من يملك المفتاح، وتغيير بايت واحد من الرسالة يغيّر القيمة كلياً. وفي الغالب تكون HMAC-SHA256، بسرّ لا يعرفه إلا المزوّد والمستهلك، على الجسم الخام (Raw body) كما هو تماماً، وكثيراً ما يسبقه طابع زمني (Timestamp) بتوقيت Unix، أي عدد الثواني منذ 1 يناير 1970 بالتوقيت العالمي (UTC). ويعيد المستهلك حساب HMAC على البايتات التي استلمها ويقارن. والتطابق يدل على أن المرسل يعرف السر وأن الجسم لم يتغير؛ ورفض الطوابع الزمنية القديمة يحدّ من إعادة إرسال طلب ملتقَط. ويوقّع المزوّدون عادةً كل محاولة من جديد، فتحمل إعادة المحاولة بعد ساعات طابعاً زمنياً جديداً. وتحقّق على البايتات الخام: فالمعالج الذي يعيد تسلسل JSON (Re-serialization) أولاً يرفض تسليمات صالحة مسافاتها أو ترتيب مفاتيحها غير معتاد. والمخطط أدناه توضيحي: فكل مزوّد يوثّق ترويساته وصيغته الخاصة.
secret shared value from the provider settings (test mode)
timestamp 1790000000, sent in the Pay-Timestamp header
input "1790000000." + raw body bytes, exactly as received
signature hex( HMAC-SHA256(secret, input) ), sent in Pay-Signature
consumer recomputes over the raw bytes it received, compares with the
header, and rejects a timestamp outside the tolerance windowالاختبار بنقطة التقاط
نقطة الالتقاط (Capture endpoint)، وتُسمّى كثيراً Request bin، عنوان URL عام مؤقت يسجّل كل طلب يصله. سجّله عنواناً لـ Webhook في وضع الاختبار (Test mode) لدى المزوّد، ثم أطلق حدثاً، فترى التسليم الحقيقي: الطريقة والترويسات والجسم الخام والتوقيت. وهذا يختبر جانب المزوّد قبل أن يوجد المستهلك: قارن الحمولة بالتوثيق، وتحقّق من توقيعها بسرّ الاختبار.
ثم يصبح التسليم الملتقَط بيانات اختبار للمستهلك. أعد إرساله (Replay)، أي أرسل الطريقة والترويسات والجسم الخام نفسها مرة أخرى، بـ cURL مثلاً: دون تغيير وداخل نافذة الطابع الزمني لاختبار التكرار، وبعد انقضاء النافذة لاختبار رفض التسليمات القديمة، ومع تغيير بايت واحد لاختبار الرفض. فداخل النافذة لا يكون خاطئاً إلا ما غيّرته أنت؛ أما خارجها فالطابع الزمني القديم وحده يسبّب الرفض. ووقّع نسخك الخاصة بأداة HMAC. ولرؤية إعادة المحاولة، أفشِل التسليم عمداً (أوقف المستهلك، أو اجعله يرد بـ 503، أو وجّه المزوّد إلى نقطة نهاية تعيد رمز 5xx)، ثم اقرأ المحاولات في سجل التسليم لدى المزوّد.
- ترد نقطة الالتقاط عادةً بـ
200على كل شيء: فهي تثبت ما أرسله المزوّد، لا أن المستهلك لديك يعمل. - استخدم وضع الاختبار وأسرار الاختبار وبيانات مصطنعة فقط، لأن التسليمات الملتقَطة تُخزَّن على خادم لبعض الوقت. واطلب تدوير أي سرّ يتسرّب.
جرّبها: مختبِر Webhook جرّبها: فاحص Webhook جرّبها: منسّق حمولات Webhook جرّبها: محاكي رموز حالة API
مثال تطبيقي: اختبارات Webhook للدفع
المتطلب: «عند وصول حدث payment.succeeded صالح، غيّر حالة الطلب إلى مدفوع وأرسل إيصالاً واحداً. التسليمات موقّعة بـ HMAC-SHA256 على الطابع الزمني والجسم، وتُرفض الطوابع الزمنية الأقدم من 300 ثانية. ويعيد المزوّد المحاولة عند أي استجابة غير 2xx وعند أي استجابة تتأخر أكثر من 10 ثوانٍ». وهذا تسليم ملتقَط:
- W1: تسليم جديد صالح ←
2xxخلال 10 ثوانٍ، والطلب 1042 مدفوع (PAID)، وإيصال واحد. - W2: إعادة إرسال التسليم نفسه، وW3: نسختان في وقت واحد ←
2xx، ويبقى إيصال واحد ومعرّف حدث واحد مخزَّن. - W4: تغيير المبلغ إلى "1.00" دون إعادة التوقيع، أو غياب التوقيع، أو سرّ آخر ←
4xx، والطلب دون تغيير، والرفض مسجَّل. - W5: توقيع صالح وطابع زمني عمره 300 ثانية ثم 301 ثانية ← يُقبل ثم يُرفض (تحليل القيم الحدّية بالصيغة الثنائية: عبارة «الأقدم من 300» تُبقي 300 في القسم المقبول). ويزداد العمر أثناء انتقال الطلب، لذا اسأل كيف يُقاس ويُقرَّب.
- W6: معالجة
payment.refundedقبل وصولpayment.succeededالمُعاد ← يبقى الطلب مُسترَداً (REFUNDED). - W7:
503على المحاولة الأولى ← يعيد المزوّد المحاولة فتنجح، وإيصال واحد.
POST /webhooks/payments HTTP/1.1
Host: shop.example
Content-Type: application/json
Pay-Timestamp: 1790000000
Pay-Signature: sha256=<hex HMAC-SHA256 of "1790000000." + raw body>
{
"id": "evt_7f3a91",
"type": "payment.succeeded",
"data": { "order_id": 1042, "amount": "190.00", "currency": "SAR" }
}أخطاء شائعة
- اختبار تسليم واحد سليم، وعدم اختبار التكرار أو النسخ المتوازية أو الأحداث بغير ترتيبها أو فشل المحاولة الأولى أبداً.
- توقّع رمز خطأ للحدث المكرر: فالمزوّد سيعيد محاولته. والمكرر يُرَد عليه بـ
2xxويُتجاهَل. - إغفال زمن الاستجابة: العمل البطيء قبل الرد (البريد وملفات PDF والخدمات الأخرى) يؤدي إلى انتهاء مهلة المزوّد، فيعيد محاولة حدث عولج. تحقّق، سجّل، أقِرّ بالاستلام، ثم عالج.
- اختبار قبول التوقيع الصالح فقط، دون التأكد من رفض التسليمات المعدّلة وغير الموقّعة والقديمة والموقّعة بسرّ خاطئ.
- إرسال بيانات عملاء حقيقية أو سرّ إنتاج إلى نقطة التقاط عامة، أو لصقه في تقرير خطأ.
من منظور ضمان الجودة
ابدأ عند التخطيط للتكامل. اطلب توثيق Webhooks لدى المزوّد (أنواع الأحداث، ومخطط الحمولة، وطريقة التوقيع، والمهلة، وجدول إعادة المحاولة، والترتيب)، واسأل المطورين: أي معرّف هو مفتاح تساوي الأثر؟ بماذا يرد المعالج إذا توقفت خدمة يعتمد عليها؟ ومن يُنبَّه حين تنفد المحاولات؟
سجّل في حالات الاختبار وتقارير الأخطاء: معرّف الحدث، ووقت المحاولة، والترويسات مع إخفاء الأسرار، والجسم الخام، ورمز حالة المستهلك وزمن استجابته، والحالة الناتجة: الصفوف والرسائل والمخزون. فعيوب التوقيع كثيراً ما لا تتكرر إلا بالبايتات نفسها، لذا أرفق التسليم الملتقَط الخام.
تمرين عملي
صمّم اختبارات Webhook للشحنات
ترسل شركة توصيل أحداث shipment.delivered إلى متجرك عبر Webhook، وتوثيقها أدناه. يجب أن يغيّر المتجر حالة الطلب إلى «تم التوصيل» ويرسل إلى العميل إشعاراً واحداً. اكتب حالات الاختبار، وبيّن كيف ستنفذ كلاً منها.
Webhooks: أحداث الشحنات
- الطريقة POST وجسم JSON؛ لكل حدث "id" فريد، وإعادة المحاولة تستخدم المعرّف نفسه.
- الترويسة Ship-Signature: قيمة HMAC-SHA256 بصيغة hex لـ timestamp + "." + الجسم الخام،
والطابع الزمني في Ship-Timestamp (ثوانٍ Unix)؛ ويُرفض ما هو أقدم من 300 ثانية.
وكل محاولة، ومنها إعادات المحاولة، تُوقَّع بطابع زمني جديد.
- عدم وصول 2xx خلال 10 ثوانٍ = فشل. حتى 8 إعادات محاولة خلال 24 ساعة.
- قد تصل الأحداث أكثر من مرة وبأي ترتيب.- اكتب الفحوص التي يمكنك إجراؤها على تسليم ملتقَط.
- اكتب حالات المستهلك للتكرار والترتيب والتوقيع وإعادة المحاولة، مع الاستجابة والحالة المتوقعتين.
- حدّد لكل حالة هل تحتاج إلى المزوّد الحقيقي في وضع الاختبار، أو تسليم ملتقَط يُعاد إرساله أو يُعدَّل، أو طلب موقَّع جديد.
- اكتب سؤالين على الأقل للمطورين.
إظهار الإجابة النموذجية
التسليم الملتقَط في وضع الاختبار: POST، وContent-Type: application/json، ووجود الترويستين، وحقول الجسم كما وُثّقت، وتوقيع يتحقق بسرّ الاختبار.
المستهلك: (1) حدث جديد صالح ← 2xx خلال 10 ثوانٍ، والطلب DELIVERED، وإشعار واحد؛ (2) إعادة إرساله ← 2xx دون إشعار ثانٍ؛ (3) نسختان بالتوازي ← إشعار واحد؛ (4) تغيير بايت في الجسم ← 4xx دون تغيير؛ (5) دون توقيع أو بسرّ خاطئ ← 4xx؛ (6) طابع زمني عمره 300 ثانية ثم 301 ثانية ← يُقبل ثم يُرفض (الصيغة الثنائية لتحليل القيم الحدّية)؛ (7) shipment.in_transit بعد shipment.delivered ← يبقى DELIVERED؛ (8) 503 على المحاولة الأولى، و(9) معالج أبطأ من 10 ثوانٍ ← تُعاد المحاولة ويبقى الإشعار واحداً.
المزوّد الحقيقي: 1 و8 و9 (اقرأ سجل التسليم لديه). تسليم ملتقَط يُعاد إرساله: 2 و3. تسليم ملتقَط معدَّل: 4، و5 بعد حذف الترويسة. أرسل الحالات من 2 إلى 5 خلال 300 ثانية من الالتقاط، وإلا كان الطابع الزمني القديم، لا الشرط المختبَر، هو سبب الرفض. طلب موقَّع جديد بأداة HMAC وبطابع زمني تختاره: 5 بسرّ خاطئ، و6، و7.
أسئلة: كيف يُقاس عمر الطابع الزمني ويُقرَّب، وهل يُقبل طابع عمره 300 ثانية بالضبط؟ أي معرّف هو مفتاح تساوي الأثر، وهل عليه قيد تفرّد؟ هل يُرسل الإشعار قبل الرد بـ 2xx أم بعده؟
أسئلة شائعة
ما الفرق بين Webhook والاستطلاع الدوري لواجهة API؟
في الاستطلاع الدوري (Polling) يستدعي المستهلك واجهة المزوّد مراراً ليسأل هل تغيّر شيء. أما في Webhook فيستدعي المزوّدُ المستهلكَ عند وقوع الحدث. تقلّل Webhooks التأخير والاستدعاءات الضائعة، لكنها تحتاج إلى نقطة نهاية عامة وفحص للتوقيع ومعالجة للتكرار؛ وكثير من التكاملات تقرأ أيضاً من الواجهة البرمجية لتعويض الأحداث الفائتة.
ما رمز الحالة الذي يجب أن تعيده نقطة نهاية Webhook؟
رمز 2xx، وعادةً 200 أو 204، بمجرد التحقق من التسليم وتسجيله بأمان، حتى للحدث المكرر الذي عولج من قبل. ورمز 4xx للتسليم الذي ترفضه، كالتسليم ذي التوقيع غير الصالح. وراجع توثيق المزوّد: فكثير من المزوّدين يعيدون المحاولة عند أي استجابة غير 2xx، فقد لا يوقف رمز 4xx إعادة المحاولة.
هل يستطيع المزوّد إرسال Webhooks إلى localhost على جهازي؟
لا. فخوادم المزوّد لا تصل إلى جهاز داخل شبكتك الخاصة. التقط التسليم بنقطة التقاط عامة ثم أعد إرساله إلى المستهلك المحلي، أو استخدم نفقاً (Tunnel) يوافق عليه فريقك، أو أداة سطر أوامر تمرّر أحداث الاختبار إن كان المزوّد يوفرها.
إذا كانت نقطة النهاية تستخدم HTTPS، فهل ما زال التوقيع ضرورياً؟
نعم. يحمي HTTPS الطلب أثناء انتقاله، لكن أي أحد يستطيع إرسال طلب إلى عنوان HTTPS عام. أما التوقيع فيثبت أن المرسل يعرف السر المشترك وأن الجسم لم يُعدَّل، وهذا ما لا يخبر به HTTPS وحده المستهلكَ.
المراجع
- RFC 9110: HTTP Semanticsمعاني أساليب HTTP وحقول الترويسات ورموز الحالة والتفاوض على المحتوى، بغضّ النظر عن إصدار البروتوكول.
- RFC 2104: HMAC: Keyed-Hashing for Message Authenticationبناء HMAC: كيف يُنتج مفتاح سري ودالة تجزئة رمز مصادقة للرسالة.
- ISTQB Certified Tester Foundation Level (CTFL) v4.0المنهج التأسيسي لاختبار البرمجيات: مستويات الاختبار وأنواعه وتقنيات تصميم الاختبارات وإدارة الاختبار والأدوات.
تدرّب بأدوات متقن
احصل على عنوان URL فريد، وأرسل إليه طلبات webhook، وافحص الطريقة والترويسات والجسم والتوقيت في الوقت الفعلي.
افحص عمليات تسليم webhook الملتقطة بعمق: الترويسات والتوقيعات وجسم JSON والتوقيت وبيانات إعادة الإرسال.
تحقّق من توقيعات webhook (بأنظمة HMAC-SHA256/SHA1 وStripe وGitHub وShopify وSlack) باستخدام سرّك — محلياً.
توليد توقيعات HMAC (SHA-256/384/512/SHA-1) لتوقيع طلبات webhook وواجهات API.
نسّق حمولات webhook من Stripe وGitHub وShopify وSlack وTwilio وغيرها، وجمّلها، وأضف إليها شروحاً توضيحية.
احصل على نقطة نهاية عامة تعيد أي رمز حالة HTTP، مع تأخير اختياري وجسم JSON مخصّص، لاختبار معالجة الأخطاء في العميل.
للتعمّق أكثر
تقدّمك في هذا الموضوع
سجّل الدخول لتتابع تقدّمك
علّم الموضوعات مكتملة، وتابع من حيث توقفت، وافتح امتحانات التخصص.