وصلت الرسالة صباح يوم أحد، أعادت توجيهها مديرة العمليات في مؤسسة أوروبية غير ربحية بسطر واحد: يرجى الاطلاع أدناه واتخاذ اللازم. وتحته رسالة آلية من Stripe تقول إنها عجزت منذ عدة أيام عن إيصال أحداث Webhook إلى موقع التبرعات، وإن تسع عشرة محاولة قد فشلت، وإنها ستتوقف عن إرسال الإشعارات إلى هذا الـ endpoint خلال أسبوع إن لم يتغير شيء.
بالنسبة لجهة تستقبل تبرعات لمرة واحدة وتبرعات شهرية عبر ووردبريس، ليس هذا تحذيرًا شكليًا. فالـ Webhooks هي الطريقة التي يعرف بها الموقع أن الدفع قد تم فعلًا. ومن دونها تبقى التبرعات معلّقة، ولا يتلقى المتبرعون إيصالاتهم، ولا تُسجَّل التجديدات الشهرية، رغم أن الأموال تواصل الوصول إلى Stripe.
هذه قصة تتبعنا للعطل، ولماذا كانت أول مشكلة وجدناها حقيقية لكنها لم تكن السبب، والتعديل الصغير الذي أصلح الأمر دون تعطيل أي حماية في الموقع.
ما وظيفة Webhook في Stripe ولماذا يهم فشله
عندما يدفع المتبرع ينتقل المتصفح إلى Stripe ثم يعود. ولا يستطيع الموقع الوثوق بما يقوله المتصفح عن الدفع، لذلك تتصل Stripe بالموقع مباشرة من خادم إلى خادم بحدث مثل payment_intent.succeeded. ويجب أن يرد الموقع برمز حالة HTTP بين 200 و299، وأي رد آخر يُعد فشلًا، فتعيد Stripe المحاولة على فترات متزايدة لمدة تصل إلى ثلاثة أيام قبل أن تتخلى عن الحدث.
كانت إضافة التبرعات هنا GiveWP، وهي تستقبل الأحداث على عنوان ينتهي بـ ?give-listener=stripe. والموقع متعدد اللغات، فكان هناك endpoint نشطان، أحدهما تحت /de/ والآخر تحت /fr/. وكلاهما كان يفشل، مع أن رسالة Stripe ذكرت واحدًا فقط.

الاكتشاف الأول: مشكلة حقيقية لكنها ليست السبب
أظهرت لوحة تحكم ووردبريس فورًا خطأ تفعيل. فقد أُوقفت إضافة Recurring Donations لأنها تتطلب إصدارًا أحدث من GiveWP مما هو مثبت. حُدّثت الإضافة ولم تُحدَّث الإضافة الرئيسية، فعطّلها ووردبريس بصمت. كما أن ترخيصها لم يكن سيُجدَّد، فتوقفت عن تلقي التحديثات.
بدا ذلك تفسيرًا مثاليًا، وكان لا بد من إصلاحه على أي حال. أخذنا نسخة احتياطية كاملة، وحدّثنا GiveWP، وأعدنا تفعيل الإضافة. فتعطلت واجهة الموقع فورًا بخطأ فادح. وأشار تتبع الخطأ إلى نافذة منبثقة تحتوي على كود قصير لنموذج تبرع قديم لم يعد موجودًا. عطّلنا الإضافة لإعادة الموقع، وصححنا النافذة المنبثقة، ثم أعدنا التفعيل وفحصنا الصفحة الرئيسية وصفحة التبرع وكل نسخ اللغات.
عاد الموقع سليمًا وعاد خيار التبرع الشهري. لكن الـ Webhooks ظلت تفشل.
قراءة الخطأ الحقيقي بدل التخمين
أسرع طريقة للتوقف عن التخمين هي فتح عملية إرسال فاشلة في لوحة Stripe، ضمن Developers ثم Webhooks، وقراءة أمرين: رمز الحالة ونص الاستجابة. كان الرمز هنا 403 Forbidden، ولم يكن نص الاستجابة خطأ من ووردبريس إطلاقًا، بل صفحة HTML عنوانها Just a moment… تحمّل سكربتات من challenges.cloudflare.com.

تلك الصفحة هي تحدي المتصفح في Cloudflare، وهي تطلب من المتصفح إثبات أنه إنسان قبل تمرير الطلب. يجتازها الشخص الذي يستخدم Chrome في ثانية، أما خوادم Stripe فليست متصفحًا ولا تستطيع حلها، لذا كان كل Webhook يُوقف عند الحافة. لم يصل الطلب إلى ووردبريس قط، ولهذا لم يظهر أي شيء في سجلات الموقع.
تحديد ميزة Cloudflare التي تحجب Stripe
يمكن لعدة ميزات في Cloudflare أن تتحدى الزيارات، والحل يعتمد على الميزة المسؤولة. في Security ثم Analytics ثم Events صفّينا النتائج حسب سلسلة الاستعلام give-listener، فظهر مع كل طلب الإجراء Managed Challenge والخدمة Security level.

وكشفت القائمة نفسها أمرًا آخر: زوار عاديون من سويسرا وفرنسا وألمانيا كانوا يتلقون التحدي أيضًا على صفحات عادية. فقد ضُبط مستوى الأمان للنطاق على درجة عالية جدًا، ربما بعد هجوم سابق، ولم ينتبه أحد إلى أنه يحجب مزوّد الدفع كذلك.
الحل: قاعدة Skip محددة بدل إضعاف الموقع
كان خفض مستوى الأمان سيحل المشكلة، لكنه سيغيّر حماية الموقع كله لأجل عنوان واحد. والحل الأنظف قاعدة مخصصة تسمح بمرور عنوان الـ Webhook وحده وتُبقي كل ما عداه كما هو.
- اذهب إلى Security ثم Security rules وأنشئ قاعدة مخصصة جديدة.
- امنحها اسمًا واضحًا، مثل Allow Stripe webhooks.
- اضبط الشرط: URI Query String وcontains و
give-listener=stripe. - اختر الإجراء Skip وفعّل All remaining custom rules.
- افتح More components to skip وفعّل Security Level وBrowser Integrity Check.
- ضع القاعدة في المرتبة الأولى ثم انشرها.
(http.request.uri.query contains "give-listener=stripe")

التفصيل الذي يسهل إغفاله هو الخطوة الخامسة. فالنسخة الأولى من القاعدة تخطّت Browser Integrity Check فقط، وأظهرت معاينة الـ API القيمة "products": ["bic"]، وكانت الـ Webhooks ستستمر في الفشل. سجل الأحداث قال إن الحجب يأتي من Security level، لذا هذا هو المكوّن الذي يجب تخطيه. ملاحظة للخطة المجانية: إذا ذكر السجل Bot Fight Mode فلا تستطيع قاعدة Skip تجاوزه، ويجب إيقاف هذا الإعداد نفسه.
هل هذا آمن؟ القاعدة لا تثق بالطلب ثقة عمياء، فـ GiveWP يتحقق من كل Webhook بمفتاح التوقيع الخاص بـ Stripe، وأي طلب مزوّر يصل إلى المستقبِل يرفضه ووردبريس. لم تكن مصادقة Stripe يومًا مهمة Cloudflare، بل مهمة التوقيع.
التأكد من الحل واستعادة التبرعات الفائتة
أعدنا إرسال حدث فاشل من لوحة Stripe فعاد بالرمز 200 OK. وخلال ساعة بدأت محاولات Stripe التلقائية بالنجاح، ووسمت اللوحة الإخفاقات السابقة بعبارة Delivery recovered. وفي اليوم التالي أرسلت Stripe بريدًا يؤكد تعافي الـ endpoint.

ملاحظتان عمليتان من مرحلة التنظيف. أولًا، زر إعادة الإرسال لا يعمل إلا على أحدث محاولة لكل حدث، فبالنسبة للأحداث التي تخلّت عنها Stripe افتح الحدث نفسه وأعد إرساله من هناك. ثانيًا، طابِق الأموال لا الـ Webhooks فقط: قارن كل دفعة في Stripe منذ بدء العطل بسجلات التبرعات في ووردبريس. في هذه الحالة كانت كل الدفعات الناجحة مسجلة بالفعل، وفشلت دفعة واحدة فعلًا لعدم كفاية الرصيد فوُسمت بالفاشلة، وحُدّدت دفعتان تجريبيتان صغيرتان أُجريتا أثناء التحقيق كي لا تُحسبا تبرعات.
دروس لكل موقع ووردبريس يستقبل مدفوعات
- اقرأ نص الاستجابة أولًا. خطأ 403 يحتوي على HTML من شبكة CDN ليس خللًا في التطبيق، ولن يصلحه أي تصحيح للإضافات.
- يجب أن تتوافق إعدادات الأمان مع Webhooks الدفع. رفع مستوى الأمان بعد هجوم أمر معقول، أما نسيان أنه يطال الاتصالات بين الخوادم فهو ما يضيّع التبرعات.
- اسمح بالمسار لا بالعالم كله. قاعدة Skip محدودة بالمستقبِل تُبقي بقية الموقع محميًا.
- تراخيص الإضافات المنتهية خطر بطيء. فهي توقف التحديثات، وقد يعطّل تحديث النواة التالي الإضافة دون إنذار.
- راقب التنبيهات. تنبّه Stripe لنحو أسبوع قبل تعطيل الـ endpoint، فتأكد من وصول هذه الرسائل إلى من يستطيع التصرف.
كتبنا عن حالة مشابهة اختلف فيها التخزين المؤقت وووردبريس على التوقيت في خطأ 403 متقطع سببه مدة التخزين المؤقت، وعن الفحوص الدورية التي تكشف مثل هذه المشكلات مبكرًا في ما الذي تشمله صيانة المواقع فعلًا.
الأسئلة الشائعة
لماذا تعيد Webhooks الخاصة بـ Stripe الخطأ 403 بينما يعمل الموقع في متصفحي؟
لأن متصفحك يجتاز تحدي Cloudflare وخوادم Stripe لا تستطيع ذلك. إذا ذكر نص الاستجابة الفاشلة Just a moment أو challenges.cloudflare.com فالطلب يُوقف في Cloudflare قبل وصوله إلى ووردبريس.
هل الأفضل السماح بعناوين IP الخاصة بـ Stripe؟
يمكن ذلك باستخدام قائمة العناوين التي تنشرها Stripe، لكنها تتغير وتحتاج إلى متابعة. قاعدة Skip على مسار الـ Webhook أبسط، ولأن الإضافة تتحقق من التوقيع فهي آمنة بالقدر نفسه عمليًا.
هل أخسر تبرعات أثناء فشل الـ Webhooks؟
عادةً تحصّل Stripe الأموال، لكن الموقع لا يسجلها. فتبقى التبرعات معلّقة ولا تُرسل الإيصالات وتغيب التجديدات المتكررة عن السجلات حتى تُسلَّم الأحداث أو تُصحَّح السجلات يدويًا.
إلى متى تواصل Stripe إعادة المحاولة؟
في الوضع الفعلي لمدة تصل إلى ثلاثة أيام على فترات متزايدة. وإذا استمر فشل الـ endpoint ترسل Stripe تحذيرًا بالبريد ثم تعطّله في النهاية.
هل تدير تبرعات أو متجرًا أو عضويات على ووردبريس وتصلك تنبيهات دفع لا تفهمها؟ نحن نجري هذا النوع من التحقيقات تحديدًا ضمن خدمات تطوير ووردبريس والصيانة. تواصل معنا وسنخبرك بوضوح بما هو معطّل وما يلزم لإصلاحه.