Webhook — qabul qilish va yuborish

~15 daq · Express · API Arxitekturasi

Muammodan boshlaylik

Foydalanuvchi to'lov qildi. To'lov muvaffaqiyatli bo'ldimi? Siz Payme, Click yoki Stripe dan javob kutasiz.

Bir yondashuv: har 5 soniyada so'rov yuborib tekshirish:

// ❌ — polling: resursni isrof qiladi
setInterval(async () => {
  const status = await checkPaymentStatus(paymentId);
  if (status === 'paid') { /* ... */ }
}, 5000);

100 foydalanuvchi to'lov qilsa — 100 ta interval, har 5 soniyada 100 ta so'rov. Ko'pchiligi "hali to'lanmagan" javob qaytaradi — befoyda.

Webhook bu muammoni ag'darib tashlaydi: siz tekshirmaysiz, ular sizga xabar beradi.


Webhook nima?

Webhook — biror hodisa sodir bo'lganda tashqi xizmat sizning serveringizga POST so'rov yuboradi.

Hayotiy o'xshatma

Ikki usul:

  • Polling (eski usul): Har kuni pochtaxonaga borib "xat bormi?" deb so'rash.
  • Webhook (zamonaviy): Pochtachi xat kelganda eshikni qo'ng'iroq qiladi.
Polling:   Siz → Payme (har 5 sek) → "yo'q" → "yo'q" → "yo'q" → "ha"
Webhook:   Payme → Siz (to'langan zahoti) → darhol

Arxitektura

project/
├── routes/
│   └── webhooks.js      ← webhook endpointlar
├── services/
│   └── paymentService.js ← to'lov logikasi
└── app.js

1-qism: Webhook qabul qilish

Oddiy webhook endpoint

// routes/webhooks.js
const express = require('express');
const router = express.Router();

router.post('/payment', async (req, res) => {
  const { event, data } = req.body;

  // 1. Darhol 200 qaytarish — muhim!
  res.status(200).json({ received: true });

  // 2. Keyin logikani bajarish
  if (event === 'payment.success') {
    await handlePaymentSuccess(data);
  }
});

module.exports = router;

Nima uchun avval 200 qaytariladi? Payme yoki Stripe sizdan 200 kutadi. Agar 5-30 soniya ichida 200 kelmasa — webhook muvaffaqiyatsiz deb hisoblanadi va qayta yuboriladi. Logika uzoq ishlasa (email yuborish, DB yozish) — bu muammoga olib keladi. Shuning uchun avval qabul qildim deb aytamiz, keyin ishlaymiz.


Imzo tekshirish (Signature Verification)

Webhook dan kelgan so'rov haqiqatdan Stripe/Payme dan kelganmi? Har kim sizning endpointingizga POST yuborishi mumkin.

Tashqi xizmatlar so'rovni imzo (signature) bilan yuboradi — maxfiy kalit bilan yaratilgan hash:

// routes/webhooks.js
const crypto = require('crypto');

const verifySignature = (req) => {
  const secret    = process.env.WEBHOOK_SECRET;    // ← Stripe dashboard dan olingan
  const signature = req.headers['x-stripe-signature']; // ← Stripe yuborgan imzo
  const payload   = req.rawBody;                   // ← xom so'rov tanasi

  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  return signature === `sha256=${expected}`;
};

router.post('/stripe', express.raw({ type: 'application/json' }), (req, res) => {
  //                   ↑ rawBody kerak — express.json() emas!

  if (!verifySignature(req)) {
    return res.status(401).json({ error: 'Imzo noto\'g\'ri' });
  }

  const event = JSON.parse(req.body);

  res.status(200).json({ received: true });

  // Hodisa bo'yicha qaror
  if (event.type === 'payment_intent.succeeded') {
    handlePaymentSuccess(event.data.object);
  }
});

express.raw() nima uchun? Imzoni tekshirish uchun so'rovning xom (parse qilinmagan) tanasi kerak. express.json() uni ob'ektga o'tkazib yuboradi — hash mos kelmay qoladi.


2-qism: Webhook yuborish

Sizning xizmatingizda hodisa sodir bo'lganda boshqa serverlarni xabardor qilish.

Yondashuv

project/
├── services/
│   └── webhookSender.js   ← webhook yuborish logikasi
└── routes/
    └── orders.js          ← hodisa sodir bo'lganda chaqiradi
// services/webhookSender.js
const crypto = require('crypto');

const sendWebhook = async (url, event, data, secret) => {
  const payload   = JSON.stringify({ event, data, timestamp: Date.now() });
  const signature = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  try {
    const res = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Signature': `sha256=${signature}`, // ← qabul qiluvchi tekshiradi
      },
      body: payload,
    });

    return res.ok;
  } catch (err) {
    console.error('Webhook yuborishda xato:', err.message);
    return false;
  }
};

module.exports = sendWebhook;
// routes/orders.js
const sendWebhook = require('../services/webhookSender');

router.post('/:id/confirm', async (req, res, next) => {
  try {
    const order = await Order.findByIdAndUpdate(
      req.params.id,
      { status: 'confirmed' },
      { new: true }
    );

    res.json({ success: true, data: order });

    // Webhook yuborish — response dan keyin, foydalanuvchi kutmaydi
    if (order.webhookUrl) {
      await sendWebhook(order.webhookUrl, 'order.confirmed', order, order.webhookSecret);
    }
  } catch (err) {
    next(err);
  }
});

Qayta urinish (Retry) mexanizmi

Webhook yuborilganda qabul qiluvchi server vaqtincha ishlamayotgan bo'lishi mumkin. Professional sistemalarda qayta urinish qo'shiladi:

// services/webhookSender.js
const sendWebhookWithRetry = async (url, event, data, secret, maxRetries = 3) => {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    const success = await sendWebhook(url, event, data, secret);

    if (success) return true;

    if (attempt < maxRetries) {
      // Har urinishda kutish vaqti ikki barobar oshadi: 1s → 2s → 4s
      const delay = 1000 * Math.pow(2, attempt - 1);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }

  console.error(`Webhook ${maxRetries} urinishdan keyin ham kelmadi: ${url}`);
  return false;
};

Bu pattern — exponential backoff: 1 soniya, 2 soniya, 4 soniya. Server bandligini ortiqcha yuklamaslik uchun.


Webhook logi — nima keldi, nima ketdi

// routes/webhooks.js
router.post('/payment', async (req, res) => {
  // Kelgan webhook ni logga yozish
  console.log('Webhook keldi:', {
    event: req.body.event,
    time: new Date().toISOString(),
    ip: req.ip,
  });

  res.status(200).json({ received: true });

  // Qayta ishlash
  await processWebhook(req.body);
});

Yoki DB ga saqlash — debug uchun foydali:

// WebhookLog modelida saqlash
await WebhookLog.create({
  source:  req.headers['user-agent'],
  event:   req.body.event,
  payload: req.body,
  receivedAt: new Date(),
});

Tez-tez uchraydigan xatolar

1. Logika tugaguncha 200 qaytarmaslik

// ❌ — uzoq operatsiya tugaguncha kutiladi
router.post('/payment', async (req, res) => {
  await sendEmail(req.body);         // ← 3 soniya
  await updateDatabase(req.body);    // ← 1 soniya
  res.status(200).json({ ok: true }); // ← 4 soniyadan keyin
});
// Stripe 3 soniyada timeout qiladi — webhook muvaffaqiyatsiz deb belgilaydi
// ✅ — avval 200, keyin logika
router.post('/payment', async (req, res) => {
  res.status(200).json({ ok: true }); // ← darhol
  await sendEmail(req.body);           // ← keyin
  await updateDatabase(req.body);
});

2. Imzoni tekshirmaslik

// ❌ — har kim POST yuborsa — ishlaydi
router.post('/payment', async (req, res) => {
  await markAsPaid(req.body.orderId); // ← xaker soxta to'lov yuborishi mumkin
});

Sababi: imzo tekshirilmasa — tajovuzkor soxta "to'lov muvaffaqiyatli" so'rovi yuborib, to'lovsiz xizmat olishi mumkin.

3. express.json() bilan imzo tekshirish

// ❌ — rawBody yo'qoladi
app.use(express.json()); // ← bu butun app uchun
router.post('/stripe', (req, res) => {
  verifySignature(req); // ← rawBody yo'q, hash mos kelmaydi
});
// ✅ — webhook route uchun alohida
router.post('/stripe', express.raw({ type: 'application/json' }), (req, res) => {
  verifySignature(req); // ← rawBody bor
});

Xulosa

  • Webhook — polling o'rniga: tashqi xizmat sizga xabar beradi
  • Darhol 200 qaytaring — keyin logikani bajaring
  • Imzoni tekshiring — crypto.createHmac bilan, har qanday POST ni qabul qilmang
  • Webhook yuborishda retry — exponential backoff bilan
  • express.raw() — Stripe va boshqa imzoli webhooklar uchun

Keyingi dars: 42-dars: WebSocket — ws moduli bilan, handshake, eventlar