یک رخداد ممکن است چند بار برسد

وقتی سرویس پرداخت نتیجه را به webhook می‌فرستد، نباید فرض کنیم پیام فقط یک بار، به ترتیب و بدون خطا می‌رسد. شبکه قطع می‌شود، پاسخ گیرنده دیر می‌رسد یا سرویس فرستنده تلاش دوباره انجام می‌دهد. اگر هر دریافت دوباره باعث ثبت سفارش یا ارسال کالا شود، تکرار یک رخداد به اثر واقعی تبدیل می‌شود. این مقاله از مستندات Stripe برای مثال استفاده می‌کند، اما مسئله دریافت قابل اتکا در اتصال بسیاری از سامانه‌ها وجود دارد. طراحی باید تکرار و قطع شدن را از ابتدا جزئی از مسیر عادی ببیند.

پیش از هر کار، اصالت پیام را بررسی کنید. در Stripe امضا با بدنه خام و راز همان endpoint بررسی می‌شود؛ تبدیل JSON و بازسازی رشته قبل از بررسی ممکن است امضا را خراب کند. راز تست و تولید را از هم جدا نگه دارید و header امضا را از درخواست اصلی بخوانید. HTTPS و بررسی امضا هر کدام نقش خود را دارند. نمونه آموزشی بدنه خام را دریافت می‌کند و پس از تأیید آن، شناسه رخداد را در یک inbox ذخیره می‌کند. این ذخیره به معنی پرداخت موفق یا اجازه تحویل کالا نیست.

دریافت پیام را از اجرای کار جدا کنید

پیشنهاد تحریریه این است که دریافت HTTP کوتاه بماند: اعتبارسنجی، ثبت پایدار و پاسخ مناسب. انجام کار طولانی را به worker بدهید. اگر قبل از ثبت پایدار پاسخ موفق بدهید و برنامه بلافاصله خاموش شود، ممکن است پیام برای همیشه از دست برود. اگر بعد از ثبت، تمام کار تجاری را داخل درخواست انجام دهید، timeout و retry محتمل‌تر می‌شود. در نمونه SQLite فقط برای نمایش inbox کوچک استفاده شده است؛ worker، زمان‌بندی تلاش دوباره و زیرساخت دیتابیس تولید باید جدا طراحی شوند.

شناسه رخداد را با قید یکتا ذخیره کنید تا دریافت هم‌زمان دوباره به دو رکورد تبدیل نشود. بررسی «اگر وجود ندارد، اضافه کن» بدون تضمین دیتابیس در برابر رقابت کافی نیست. با این حال، event_id یکتا تمام تکرارهای تجاری را حل نمی‌کند؛ ممکن است دو رخداد جدا برای یک عملیات واحد وجود داشته باشد. برای کار نهایی، کلید مناسب خود عملیات، وضعیت رکورد و تراکنش را هم بررسی کنید. idempotency یعنی اجرای دوباره اثر اضافی نداشته باشد، نه اینکه فقط پاسخ دوم را در log متفاوت بنویسیم.

worker باید پیام ثبت‌شده را بردارد، قواعد کسب‌وکار را بررسی کند و نتیجه را با وضعیت روشن ذخیره کند. اگر برای اثر بیرونی، مانند ایمیل یا ارسال کالا، فقط وضعیت را پیش از اجرا تغییر دهد، خرابی می‌تواند کار را از دست بدهد؛ اگر بعد از اجرا تغییر دهد، خرابی ممکن است اجرای دوباره بسازد. طراحی تراکنش، outbox و کلید idempotency در سرویس مقصد برای این فاصله اهمیت دارد. اطلاعات پرداخت و مشتری را به اندازه لازم ذخیره کنید و برای نگهداری و دسترسی inbox سیاست روشن داشته باشید.

نمونه کد و روش بررسی

این مثال آموزشی برای فهم مسیر پیاده‌سازی نوشته شده است. نسخه‌ها و پیش‌نیازهای ذکرشده را در محیط آزمایش بررسی کنید؛ نکات زیر مشخص می‌کنند برای استفاده عملی چه چیزهایی باید تکمیل شوند.

FastAPI؛ فقط inbox پایدار در محیط آزمایش
# python -m pip install fastapi uvicorn stripe
# Configure STRIPE_WEBHOOK_SECRET, then: uvicorn inbox_demo:app
import os, sqlite3
from contextlib import closing
import stripe
from fastapi import FastAPI, Request, HTTPException

app = FastAPI()
secret = os.environ["STRIPE_WEBHOOK_SECRET"]
with closing(sqlite3.connect("webhook-inbox.db")) as db, db:
    db.execute("""CREATE TABLE IF NOT EXISTS inbox (
      event_id TEXT PRIMARY KEY, event_type TEXT NOT NULL,
      payload BLOB NOT NULL, status TEXT NOT NULL DEFAULT 'pending'
    )""")

@app.post("/webhook")
async def webhook(request: Request):
    payload = await request.body()
    signature = request.headers.get("stripe-signature", "")
    try:
        event = stripe.Webhook.construct_event(payload, signature, secret)
    except (ValueError, stripe.SignatureVerificationError):
        raise HTTPException(400, "Invalid webhook")
    try:
        with closing(sqlite3.connect("webhook-inbox.db", timeout=5)) as db, db:
            db.execute("""INSERT INTO inbox(event_id,event_type,payload)
              VALUES (?,?,?) ON CONFLICT(event_id) DO NOTHING""",
              (event["id"],event["type"],payload))
    except sqlite3.Error:
        raise HTTPException(503, "Inbox unavailable")
    return {"received": True}

کد را در inbox_demo.py ذخیره کنید و راز امضای endpoint تست را در محیط تنظیم کنید. دو بار ارسال همان رخداد معتبر باید فقط یک رکورد pending بسازد؛ امضای غلط باید ۴۰۰ و خرابی ذخیره‌سازی باید ۵۰۳ بدهد. کد هیچ سفارشی را تکمیل نمی‌کند. worker، انتخاب نوع رخداد، کنترل مبلغ و وضعیت، محدودیت اندازه درخواست و نگهداری امن payload باید تکمیل شوند. SQLite هم‌زمان و داخل handler آموزشی است؛ برای بار تولید از مسیر دیتابیس و اجرای غیرمسدودکننده مناسب استفاده کنید.

تکرار و خرابی را جزئی از آزمون بدانید

آزمون پذیرش را با یک پیام معتبر، امضای غلط، ارسال دوباره همان event_id و توقف سرویس پس از ثبت انجام دهید. باید یک رکورد pending باقی بماند که worker بعداً بتواند ادامه دهد. سپس رخدادهای خارج از ترتیب و اثر بیرونی ناموفق را آزمایش کنید. شاخص‌ها شامل تعداد پیام پذیرفته‌شده، تأخیر پردازش، retry و پیام‌های نیازمند رسیدگی دستی هستند. نسخه خوب webhook مسیری قابل ادامه دادن پس از خرابی است؛ برگرداندن ۲۰۰ برای همه درخواست‌ها یا داشتن یک set در حافظه، این نیاز را تأمین نمی‌کند.

چک‌لیست اجرای عملی

  • بدنه را پیش از بررسی امضا parse یا بازسازی نکنید.
  • راز endpoint را از متغیر محیطی بخوانید و تست و تولید را جدا کنید.
  • شناسه یکتا را در ذخیره پایدار با قید دیتابیس ثبت کنید.
  • پاسخ موفق دریافت را از تکمیل عملیات تجاری تفکیک کنید.

توضیح‌ها و پیشنهادهای اجرایی این مطلب، تحلیل تحریریه دانشنامه لیان هستند.منابع: Stripe — Webhooks · Stripe — Verify webhook signatures · Stripe — Idempotent requests

این مطلب بازنویسی تحلیلی دانشنامه لیان بر پایه منبع اصلی است.مشاهده منبع اصلی