حول المحتوى:
خطوات بناء Webhook موثوق: التحقق من التوقيع، التعامل مع الفشل، إعادة المحاولة، تسجيل الأحداث، وأمثلة تطبيقية في بايثون.
تنفيذ Webhooks بشكل احترافي وآمن أصبح جزءًا أساسيًا من أي مشروع برمجي يتكامل مع خدمات خارجية مثل بوابات الدفع، أنظمة الإشعارات، منصات الـ SaaS وغيرها. أي خطأ في Webhooks تنفيذ قد يؤدي إلى تسريب بيانات، أو تنفيذ أوامر غير موثوقة، أو فقدان أحداث مهمة لم يتم تسجيلها.
في هذا المقال من افهم صح سنشرح خطوة بخطوة كيفية بناء Webhook موثوق، بدءًا من فهم الفكرة، مرورًا بـالتحقق من التوقيع (Signature Verification)، آليات إعادة المحاولة، التعامل مع الفشل، تسجيل الأحداث (Logging)، مع أمثلة عملية باستخدام بايثون.
الـ Webhook هو آلية تسمح لخدمة خارجية بإرسال طلب HTTP (عادة POST) إلى خادمك عندما يحدث حدث معيّن. عوضًا عن أن تسأل أنت الخدمة في كل مرة (Polling)، تقوم الخدمة بإخبارك فورًا بما حدث.
أمثلة شائعة لاستخدام الـ Webhooks:
هذا يعني أن نقطة الـ Webhook في تطبيقك هي واجهة مكشوفة للعالم الخارجي، لذلك يجب أن يكون Webhooks تنفيذ فيها آمنًا ومدروسًا من البداية.
عند تنفيذ Webhook في مشروعك، ستقوم عادة بإنشاء مسار (Route) في الـ API مثل:
/webhooks/payment/webhooks/githubهذا المسار يستقبل:
المهمة الأساسية في Webhooks تنفيذ هي:
معظم الخدمات التي تدعم Webhooks (مثل Stripe, GitHub, SendGrid) ترسل في الـ Headers توقيعًا رقميًا مبنيًا على Secret مشترك بينك وبين الخدمة. أنت بدورك تعيد حساب التوقيع بنفس الخوارزمية وتقارنه بما أرسلته الخدمة.
الفكرة العامة:
HMAC-SHA256 لتوليد توقيع بناء على الـ Secret والـ body.401 Unauthorized أو 400 Bad Request.هذا مثال توضيحي (ليس خاصًا بخدمة معيّنة، لكن يشرح الفكرة):
import hmac
import hashlib
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = b"my_super_secret_key" # اجعله من متغيرات البيئة
def verify_signature(raw_body: bytes, header_signature: str) -> bool:
if not header_signature:
return False
# إنشاء التوقيع باستخدام HMAC-SHA256
computed = hmac.new(WEBHOOK_SECRET, raw_body, hashlib.sha256).hexdigest()
# استخدم compare_digest للمقارنة بشكل آمن ضد هجمات timing attacks
return hmac.compare_digest(computed, header_signature)
@app.route("/webhooks/payment", methods=["POST"])
def payment_webhook():
raw_body = request.get_data()
header_sig = request.headers.get("X-Signature")
if not verify_signature(raw_body, header_sig):
abort(401, "Invalid signature")
event = request.get_json()
# ... معالجة الحدث هنا ...
return "", 200
استخدام hmac.compare_digest مهم لتفادي هجمات Timing Attack التي تعتمد على وقت المقارنة لكشف أجزاء من التوقيع.
لا تستخدم HTTP عادي في Webhooks تنفيذ. تأكد أن endpoint الخاص بك يعمل عبر HTTPS، حتى لا يتمكن مهاجم من قراءة أو تعديل البيانات أثناء انتقالها بين الخدمة وخادمك.
بعض الخدمات تنشر قائمة بعناوين IP التي تستخدمها لإرسال Webhooks. يمكنك:
لكن لا تعتمد على هذا وحده، بل اعتبره طبقة إضافية بجانب التحقق من التوقيع.
الخدمة عادة ترسل نوع الحدث في Header أو في الـ body (مثل event_type أو type). من الأفضل أن:
SUPPORTED_EVENTS = {"payment.succeeded", "payment.failed"}
event_type = event.get("type")
if event_type not in SUPPORTED_EVENTS:
# سجّل وارجع 200 حتى لا تعيد الخدمة المحاولة بلا داعٍ
log_warning(f"Unsupported event type: {event_type}")
return "", 200
أي Webhooks تنفيذ احترافي يجب أن يتوقّع وجود أخطاء: قاعدة بيانات غير متاحة، خدمة داخلية متوقفة، أو وقت معالجة طويل. معظم مزودي الـ Webhooks يقومون تلقائيًا بـ إعادة إرسال الطلب عند فشل الاستجابة (5xx أو Timeout).
مهم جدًا أن تكون معالجة الحدث Idempotent، أي لو تم استلام نفس الحدث مرتين لا يتم تنفيذ نفس العملية مرتين (مثل خصم مبلغ أو إنشاء سجل متكرر).
لتحقيق هذا:
id داخل الحدث).
def handle_event(event):
event_id = event.get("id")
if has_been_processed(event_id):
return # لا تعالج مرتين
save_event_as_processed(event_id)
# الآن نفّذ منطق العمل (Business Logic)
process_business_logic(event)
من الأفضل أن يكون ردك سريعًا (في أقل من ثوانٍ قليلة) حتى لا تفشل الخدمة الخارجية. لا تنفّذ عمليات ثقيلة (إرسال عشرات الإيميلات، تقارير كبيرة، استهلاك API خارجي بطيء) مباشرة داخل طلب الـ Webhook.
بدلًا من ذلك:
يمكنك استخدام البرمجة غير المتزامنة في بايثون لتحسين الأداء عند التعامل مع خدمات خارجية، وقد شرحنا هذا في مقال البرمجة غير المتزامنة في بايثون: تحسين الأداء باستخدام async و await.
حتى لو كانت الخدمة المرسِلة تعيد المحاولة، قد تحتاج أنت من جهتك إلى إعادة معالجة الحدث إذا فشل جزء من منطق العمل الداخلي.
تسجيل أحداث Webhooks مهم لسببين:
احرص على عدم تسجيل بيانات حساسة بالكامل (مثل أرقام بطاقات الائتمان، أو رموز سرية)، بل أخفِ أجزاءً منها أو لا تسجلها إطلاقًا.
لنربط كل ما سبق في مثال عملي مبسط:
import hmac
import hashlib
import logging
from datetime import datetime
from flask import Flask, request, abort, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = b"my_super_secret_key"
# إعداد اللوجر
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("webhooks")
# قاعدة بيانات مبسطة في الذاكرة (لشرح الفكرة فقط)
PROCESSED_EVENTS = set()
def verify_signature(raw_body: bytes, header_signature: str) -> bool:
if not header_signature:
return False
computed = hmac.new(WEBHOOK_SECRET, raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(computed, header_signature)
def has_been_processed(event_id: str) -> bool:
return event_id in PROCESSED_EVENTS
def save_event_as_processed(event_id: str):
PROCESSED_EVENTS.add(event_id)
def process_business_logic(event: dict):
event_type = event.get("type")
data = event.get("data", {})
if event_type == "payment.succeeded":
# مثال: تحديث حالة طلب في قاعدة البيانات
order_id = data.get("order_id")
amount = data.get("amount")
logger.info(f"Payment succeeded for order {order_id}, amount={amount}")
# update_order_status(order_id, "paid")
elif event_type == "payment.failed":
order_id = data.get("order_id")
logger.warning(f"Payment failed for order {order_id}")
# update_order_status(order_id, "failed")
else:
logger.info(f"Unhandled event type: {event_type}")
@app.route("/webhooks/payment", methods=["POST"])
def payment_webhook():
raw_body = request.get_data()
header_sig = request.headers.get("X-Signature")
if not verify_signature(raw_body, header_sig):
logger.warning("Invalid signature for webhook")
abort(401, "Invalid signature")
event = request.get_json() or {}
event_id = event.get("id")
event_type = event.get("type")
if not event_id or not event_type:
logger.warning("Missing event_id or type in webhook payload")
abort(400, "Invalid payload")
# Idempotency
if has_been_processed(event_id):
logger.info(f"Duplicate event received, ignoring: {event_id}")
return "", 200
received_at = datetime.utcnow().isoformat()
logger.info(f"Received event {event_id} of type {event_type} at {received_at}")
try:
process_business_logic(event)
save_event_as_processed(event_id)
except Exception as e:
logger.exception(f"Error processing event {event_id}: {e}")
# من الأفضل هنا إرجاع 500 حتى تعيد الخدمة المحاولة
abort(500, "Internal error while processing event")
return jsonify({"status": "ok"}), 200
هذا المثال يوضح الجوانب الأساسية في Webhooks تنفيذ: التحقق من التوقيع، التعامل مع التكرار، تسجيل الأحداث، والتعامل مع الأخطاء.
الـ Webhooks تعتمد على طلبات HTTP من الخادم الخارجي إلى خادمك، وهي Event-driven من جهة واحدة. أما WebSockets فهي قناة اتصال ثنائية الاتجاه بين العميل والخادم. إذا كان يهمك هذا النوع من الاتصال، يمكنك مراجعة: التعامل مع WebSockets في FastAPI: تطبيق عملي.
لأن مزود الخدمة يحتاج عنوان URL عام (Public)، يمكنك استخدام أدوات مثل ngrok أو localtunnel لفتح نفق لخادمك المحلي، ثم:
إذا كان Webhook endpoint لديك بطيئًا، قد تقرر الخدمة المرسلة إلغاء الإرسال أو تقليل عدد مرات إعادة المحاولة. تأكد من:
لا تتعامل مع Webhooks على أنها “إضافة جانبية”. اكتب اختبارات تغطي:
لكي يكون Webhooks تنفيذ داخل مشاريعك صحيحًا وآمنًا، تأكد من النقاط التالية:
باتباع هذه الممارسات، يمكنك دمج Webhooks في مشاريعك بثقة، سواء كنت تعمل على تطبيقات بايثون أو لغات أخرى. وإن كنت في بداية طريقك مع بايثون، فربما يفيدك الاطلاع على أهم الأخطاء الشائعة في بايثون التي يرتكبها المطورون الجدد لتفاديها أثناء بناء تطبيقات حقيقية تعتمد على Webhooks أو غيرها من التقنيات.
خطوات بناء Webhook موثوق: التحقق من التوقيع، التعامل مع الفشل، إعادة المحاولة، تسجيل الأحداث، وأمثلة تطبيقية في بايثون.
مساحة اعلانية