Webhook — qabul qilish va yuborish
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.createHmacbilan, 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