8.20-bob: Webhook'lar va idempotency
8-QISM — NestJS (chuqur) · 20-mavzu · Amaliy real mavzu
1. Kirish va motivatsiya
Payment 8.19-bob da webhook'ni qisman ko'rdik — endi uni mustaqil, to'liq mavzu sifatida o'rganamiz, chunki webhook faqat to'lovda emas, har joyda uchraydi: GitHub (push/PR hodisalari), Telegram 8.12-bob, SMS (5.18 yetkazib berish statusi), Stripe 8.19-bob, CI/CD (10), va sizning o'z tizimlaringiz orasidagi aloqa. Webhook — zamonaviy backend'ning asab tizimi: tashqi tizim sizga "nimadir sodir bo'ldi" deb o'zi xabar yuboradi (siz so'rab turishingiz — polling — shart emas).
Webhook ikki tomonlama: inbound (siz tashqi tizimdan webhook qabul qilasiz — eng keng) va outbound (siz boshqalarga webhook yuborasiz — agar sizning API'ngiz bo'lsa). Inbound webhook — eng ko'p ishlatiladigan, lekin eng ko'p xato qilinadigan joy: imzo tekshirmaslik (xavfsizlik teshigi), idempotency yo'q (takror ishlov), sekin javob (gateway retry qiladi), xato boshqaruvi yo'q (yo'qolgan hodisalar). Bu bob shularni to'g'ri qilishni o'rgatadi.
Bu bob: webhook nima va nega (polling vs webhook), inbound webhook qabul qilish, imzo tekshirish (HMAC — xavfsizlik), raw body muammosi, idempotency (takror = bir marta — to'liq), tez javob + fonda ishlash (navbat — 8.22), retry + exponential backoff + jitter, dead-letter queue, va outbound webhook (siz yuborasiz). Bu bob 8.19 (payment webhook), 8.22 (navbat), 8.6 (guard/middleware), 14 (xavfsizlik) bilan bog'liq. Webhook — har integratsiyada kerak.
O'xshatish: webhook — eshik qo'ng'irog'i (polling — har daqiqa eshikni ochib "kim keldimi?" deb qarash — samarasiz). Qo'ng'iroq (webhook) bosilsa — sizga o'zi xabar keladi (tashqi tizim chaqiradi). Lekin: eshikni ochishdan oldin kim ekanini tekshirasiz (imzo — soxta qo'ng'iroqdan himoya); bir mehmon ikki marta qo'ng'iroq qilsa, uni ikki marta kiritmaysiz (idempotency); va eshikni tez ochasiz (mehmonni kuttirmaysiz — fonda ishlaysiz). Webhook'ni to'g'ri ishlatish — aynan shu odob.
Nega muhim?
- Asab tizimi — tashqi tizimlar aloqasi (payment, GitHub, SMS).
- Real-time — polling o'rniga (samarali, tez).
- Eng ko'p xato joyi — imzo, idempotency, retry (to'g'ri qilish kerak).
- Har integratsiyada — to'lov, bildirishnoma, CI/CD.
2. Nazariya — chuqur tushuntirish
2.1. Polling vs Webhook
POLLING (siz so'rab turasiz):
Har 10 soniyada "yangilik bormi?" ko'pincha "yo'q" (behuda so'rov)
Samarasiz (minglab behuda so'rov), kechikish (10 soniyagacha)
WEBHOOK (tashqi tizim o'zi xabar beradi):
Hodisa sodir bo'lganda tashqi tizim SIZNI chaqiradi (POST)
Tez (darrov), samarali (faqat kerakda), real-time
Tashqi tizim qo'llab-quvvatlasa — har doim webhook (polling — oxirgi chora)Polling vs Webhook: polling — siz muntazam so'rab turasiz (samarasiz — ko'p behuda so'rov, kechikish); webhook — tashqi tizim hodisa sodir bo'lganda o'zi sizni chaqiradi (POST — tez, samarali, real-time). Webhook — afzal (tashqi tizim qo'llab-quvvatlasa). Polling — faqat webhook yo'q bo'lganda (yoki reconciliation — 8.19: 2.15). Bu — zamonaviy integratsiya asosi.
2.2. Inbound vs Outbound webhook
INBOUND (siz QABUL qilasiz — eng keng):
Tashqi tizim POST sizning /webhook siz ishlaysiz
Misol: Stripe to'lov, GitHub push, SMS status (8.19, 5.18)
OUTBOUND (siz YUBORASIZ — sizning API'ngiz bo'lsa):
Sizning tizim POST mijoz /webhook mijoz ishlaydi
Misol: sizning to'lov tizimingiz mijozga "to'landi" deb xabar berishInbound (qabul — eng keng): tashqi tizim sizning endpoint'ni chaqiradi (Stripe, GitHub). Outbound (yuborish — agar sizning platformangiz bo'lsa): siz mijozlaringizga webhook yuborasiz (ular reaksiya qiladi). Inbound — diqqat imzo/idempotency'da (2.4, 2.5); outbound — diqqat retry/ishonchli yetkazishda 2.8-bob. Ko'pincha inbound bilan boshlanadi.
2.3. Webhook endpoint (qabul qilish asosi)
@Controller("webhooks")
export class WebhooksController {
@Post("github")
@HttpCode(200) // tez 200 qaytarish (2.6)
async github(@Body() payload: any, @Headers("x-hub-signature-256") sig: string) {
// 1. Imzo tekshir 2.4-bob 2. idempotency 2.5-bob 3. navbatga 2.6-bob 4. 200
return { received: true };
}
}Webhook endpoint — oddiy POST controller 8.1-bob. Lekin 4 muhim bosqich: imzo tekshir 2.4-bob, idempotency 2.5-bob, fonda ishlash (navbatga — 2.6), tez 200 qaytarish 2.6-bob. Header'da imzo (
x-hub-signature-256— GitHub). Bu — webhook'ning to'g'ri tuzilishi (har bosqich muhim).
2.4. Imzo tekshirish (HMAC — xavfsizlik)
import { createHmac, timingSafeEqual } from "crypto";
// HMAC-SHA256 imzo tekshirish (Stripe/GitHub/Shopify standart)
function imzoTekshir(rawBody: Buffer, sig: string, secret: string): boolean {
const kutilgan = createHmac("sha256", secret) // sirli kalit bilan
.update(rawBody) // RAW body (2.7)
.digest("hex");
const sigBuf = Buffer.from(sig);
const kutBuf = Buffer.from(`sha256=${kutilgan}`);
if (sigBuf.length !== kutBuf.length) return false;
return timingSafeEqual(sigBuf, kutBuf); // timing-attack himoyasi!
}Imzo (HMAC) tekshirish (xavfsizlik — eng muhim): webhook URL ochiq har kim soxta yuborishi mumkin. Imzo — yuboruvchi sirli kalitni biladi (HMAC-SHA256 — payload + secret hash). Siz qayta hisoblab solishtirasiz.
timingSafeEqual(oddiy===emas — timing-attack himoyasi — 14). Imzo mos kelmasa rad (401). Stripe/GitHub/Shopify — HMAC-SHA256 standart (lekin header/format har xil). Imzosiz webhook — xavfsizlik teshigi.
2.5. Idempotency (takror = bir marta — to'liq)
// Gateway AT-LEAST-ONCE yetkazadi bir hodisa bir necha marta kelishi mumkin
@Entity()
export class ProcessedWebhook {
@PrimaryColumn() eventId: string; // UNIQUE (provider event ID)
@CreateDateColumn() processedAt: Date;
}
async webhookIshla(eventId: string, ish: () => Promise<void>) {
try {
await this.repo.insert({ eventId }); // UNIQUE — ikkinchi marta xato
} catch (e) {
if (e.code === "23505") return; // allaqachon ishlangan o'tkazib yubor
throw e;
}
await ish(); // birinchi marta bajar
}Idempotency (8.19: 2.8 — to'liq): webhook at-least-once yetkaziladi (bir hodisa bir necha marta kelishi mumkin — retry). Yechim: event ID ni unique saqlash ikkinchi marta kelsa, o'tkazib yuborish.
insert+ unique constraint (atomik — race condition'siz;findOne+inserto'rniga — ikki so'rov orasida boshqasi kirib qolishi mumkin). Idempotency'siz — ikki marta email, ikki marta buyurtma 8.19-bob. Webhook'ning majburiy qoidasi.
2.6. Tez javob + fonda ishlash (navbat — 8.22)
@Post("stripe")
@HttpCode(200)
async stripe(@Req() req: RawBodyRequest<Request>, @Headers("stripe-signature") sig: string) {
// 1. Imzo tekshir (TEZ — 2.4)
const event = this.verify(req.rawBody, sig);
// 2. Navbatga qo'shib, DARROV 200 qaytar (og'ir ish fonda — 8.22)
await this.webhookQueue.add("process", { event }, { jobId: event.id }); // jobId — idempotency
return { received: true }; // 200 darrov (< 10 soniya!)
}
// Worker 8.22-bob fonda ishlaydi (email, DB, fulfillment)Tez javob + fonda (best practice): webhook'ga tez 200 qaytarish kerak (gateway 10 soniya kutadi — sekin bo'lsa "failed" deb retry qiladi takror!). Yechim: imzo tekshir (tez) navbatga qo'sh (BullMQ — 8.22) darrov 200 worker fonda og'ir ishni bajaradi.
jobId: event.id— navbat darajasida idempotency. Bu — ishonchli webhook arxitekturasi (sekin handler — eng keng xato).
2.7. Raw body muammosi (imzo uchun)
// main.ts — raw body yoqish (imzo RAW body'dan hisoblanadi — 2.4)
const app = await NestFactory.create(AppModule, { rawBody: true });
// Yoki muayyan route uchun (Express)
app.use("/webhooks/stripe", express.raw({ type: "application/json" }));Raw body muammosi (eng keng xato): imzo RAW (xom) body'dan hisoblanadi (8.19: 2.9). Lekin NestJS/Express
json()middleware body'ni parse qiladi (raw yo'qoladi) imzo har doim xato. Yechim:rawBody: true(NestJS) yokiexpress.raw()(muayyan route). Imzo tekshirishdan keyin parse. Bu xato — webhook integratsiyasida №1 muammo (imzo doim fail).
2.8. Retry, exponential backoff, jitter (outbound)
// Outbound webhook yuborish — ishonchli yetkazish (retry)
async webhookYubor(url: string, payload: any, urinish = 0): Promise<void> {
try {
const sig = createHmac("sha256", this.secret).update(JSON.stringify(payload)).digest("hex");
await axios.post(url, payload, { headers: { "x-signature": `sha256=${sig}` }, timeout: 10000 });
} catch (e) {
if (urinish >= 5) { // 5 marta urinib bo'ldi
await this.deadLetterQueue.add(payload); // dead-letter (2.9)
return;
}
// Exponential backoff + jitter (2.8)
const kechikish = Math.pow(2, urinish) * 1000 + Math.random() * 1000; // 1s, 2s, 4s... + tasodif
await this.queue.add("retry", { url, payload, urinish: urinish + 1 }, { delay: kechikish });
}
}Retry + backoff + jitter (outbound — ishonchli yetkazish): qabul qiluvchi vaqtincha ishlamasligi mumkin qayta urinish. Exponential backoff (1s, 2s, 4s, 8s — har safar ikki barobar — qabul qiluvchini bosmaslik). Jitter (tasodifiy qo'shimcha —
Math.random()) — minglab webhook bir vaqtda fail bo'lsa, hammasi bir vaqtda retry qilmasligi uchun ("thundering herd" oldini olish). 5 marta fail dead-letter 2.9-bob. Retriable (503/timeout) vs permanent (400) farqlash.
2.9. Dead-letter queue (yo'qolmaslik)
DEAD-LETTER QUEUE (DLQ) — barcha retry tugagan hodisalar:
- 5 marta urinib ham yetkazib bo'lmadi DLQ'ga saqlanadi (yo'qolmaydi)
- Sabab tuzatilgach qayta yuborish (replay)
- Monitoring (DLQ to'lsa — ogohlantirish)
Hodisa YO'QOLMASLIGI kerak (pul/buyurtma — kritik)Dead-letter queue — barcha retry tugagan hodisalar saqlanadigan joy (yo'qolmasin). Sabab tuzatilgach (qabul qiluvchi tiklangach) — replay (qayta yuborish). DLQ monitoring (to'lib ketsa — ogohlantirish — 8.15). Hodisa yo'qolmasligi kerak (to'lov/buyurtma — kritik). BullMQ failed jobs 8.22-bob — DLQ vazifasini bajaradi. Bu — ishonchlilikning oxirgi himoyasi.
2.10. Webhook xavfsizligi (14)
Imzo (HMAC) HAR webhook'da, timingSafeEqual (2.4, 14)
HTTPS majburiy (shifrlangan)
Raw body (imzo to'g'ri — 2.7)
Timestamp tekshirish (replay-attack — eski webhook qayta yuborilmasin)
IP allowlist (agar gateway IP ma'lum bo'lsa — qo'shimcha himoya)
Idempotency (takror — 2.5)
Rate limiting (DoS — 8.16); sekin emas (10s — 2.6)
Sir noto'g'ri 401 (oshkor qilmasdan)Webhook xavfsizligi (14): imzo (timingSafeEqual — 2.4), HTTPS, raw body 2.7-bob, timestamp (replay-attack — eski webhook qayta yuborilishidan himoya — Stripe tolerance: 5 daqiqa), IP allowlist (qo'shimcha), idempotency 2.5-bob, rate limit 8.16-bob. Webhook — ochiq endpoint (xavfsizlik kritik). To'lov webhook'i 8.19-bob — eng diqqat bilan.
2.11. Best practices (webhook)
QABUL (inbound):
Imzo tekshir (HMAC, timingSafeEqual — 2.4) + raw body 2.7-bob
Idempotency (event ID unique — 2.5)
Tez 200 + navbatga (fonda — 2.6, 8.22)
Xavfsizlik (HTTPS, timestamp, rate limit — 2.10)
YUBORISH (outbound):
Imzo qo'sh (qabul qiluvchi tekshirsin)
Retry + exponential backoff + jitter 2.8-bob
Dead-letter queue (yo'qolmaslik — 2.9)
Timeout (10s); idempotency key (qabul qiluvchi uchun)2.12. Idempotency: DB vs Redis (qaysi birini)
DB (unique constraint — 2.5):
Ishonchli (crash'ga chidamli — saqlanadi), audit (kim/qachon)
Bepul (mavjud DB), transaction bilan atomik (bir tranzaksiyada ishlov + belgi)
Sekinroq (disk yozuv), jadval o'sadi (eski yozuvlarni tozalash kerak)
REDIS (SET NX + TTL):
Tez (xotira), TTL bilan avtomatik tozalanadi (masalan 24 soat)
Volatil (Redis o'chsa — belgi yo'qoladi takror xavfi)
Yuqori hajm (soniyada minglab webhook) uchun yaxshi
PUL/BUYURTMA (kritik): DB (transaction bilan — yo'qolmasin)
Yuqori hajm, "yaxshi bo'lardi" dedup: Redis (tez, TTL)
Ikkalasi: Redis (tez tekshir) + DB (ishonchli yozuv)DB vs Redis idempotency: DB unique constraint 2.5-bob — ishonchli (crash'ga chidamli, audit), pul/buyurtma uchun (
SELECT ... FOR UPDATEyoki insert bir tranzaksiyada — atomik). RedisSET key NX EX(agar mavjud bo'lmasa qo'y, TTL bilan) — tez (xotira), avtomatik tozalanadi, yuqori hajm uchun. Redis volatil (o'chsa belgi yo'qoladi) kritik operatsiyada faqat Redis'ga tayanmang. Ko'p tizim: Redis (tez filtr) + DB (yakuniy ishonch). TTL — provider retry oynasidan uzunroq (Stripe 3 kun TTL ≥ 3 kun).
2.13. Event tartibi (ordering — kelish tartibi kafolatlanmaydi)
MUAMMO: webhook'lar TARTIBSIZ kelishi mumkin (network, retry)
Misol: "created" dan OLDIN "updated" kelib qolishi mumkin!
Yoki: eski holat yangi holatdan KEYIN kelib, yangini bosib ketishi
YECHIMLAR:
1. Versiya/timestamp taqqoslash — event ichidagi vaqt/versiya bilan
(eski event kelsa e'tiborsiz qoldirish)
2. Holatni webhook'dagi ma'lumotdan EMAS, provider'dan qayta o'qish (fetch)
(event — faqat "signal"; haqiqiy holat API'dan olinadi)
3. Idempotent + kommutativ dizayn 2.14-bob — tartibga bog'liq bo'lmaslikEvent tartibi (ordering): webhook'lar kelish tartibi kafolatlanmaydi (network kechikishi, retry — "updated" "created"dan oldin kelishi mumkin). Xato: webhook'dagi holatni ko'r-ko'rona yozish eski event yangisini bosib ketadi. Yechim: (1) event versiyasi/timestampini taqqoslash (eski bo'lsa — e'tiborsiz); (2) webhook'ni faqat signal deb bilib, haqiqiy holatni provider API'dan qayta o'qish (Stripe tavsiyasi — "fetch, don't trust payload"); (3) kommutativ operatsiya (2.14 — tartibga bog'liq emas). To'lov holatida — ayniqsa muhim (refund'dan keyin succeeded kelib qolmasin).
2.14. Idempotent operatsiya dizayni
IDEMPOTENT = bir necha marta bajarilsa ham natija bir xil
(webhook at-least-once operatsiyaning O'ZI ham xavfsiz bo'lishi kerak)
balans += 100 (har chaqiruvda o'sadi — NOTO'G'RI)
status = "paid" (necha marta yozsang ham — bir xil natija)
UPSERT (bor bo'lsa yangila, yo'q bo'lsa yarat — takrorga chidamli)
"agar hali paid emas bo'lsa paid qil" (shartli — WHERE status != 'paid')
Idempotency belgisi 2.5-bob + idempotent operatsiya = ikki qatlam himoyaIdempotent operatsiya dizayni (chuqur): idempotency belgisi 2.5-bob birinchi himoya, lekin operatsiyaning o'zi ham idempotent bo'lsa — ikki qatlam ishonch. Yomon:
balans += summa(har takrorda o'sadi). Yaxshi: holatni o'rnatish (status = 'paid'— necha marta yozilsa ham bir xil), UPSERT, yoki shartli yangilash (UPDATE ... WHERE status != 'paid'— allaqachon bajarilgan bo'lsa hech narsa o'zgarmaydi). Buyurtma yaratishda — client tomonidan berilgan idempotency key bilan (bir kalit bir buyurtma). Bu — 8.19 (to'lov) va webhook'ning eng chuqur tamoyili: tarmoq ishonchsiz operatsiyani takrorga chidamli qil.
3. Sintaksis — tez ma'lumotnoma
// Imzo 2.4-bob: createHmac("sha256", secret).update(rawBody).digest("hex") + timingSafeEqual
// Raw body 2.7-bob: NestFactory.create(AppModule, { rawBody: true })
// Idempotency 2.5-bob: insert unique eventId 23505 skip
// Tez javob 2.6-bob: imzo queue.add(..., { jobId: eventId }) return 200
// Retry 2.8-bob: delay = 2^urinish * 1000 + Math.random()*1000
// Redis dedup 2.12-bob: redis.set(`wh:${eventId}`, "1", "NX", "EX", 259200) null = takror
// Ordering 2.13-bob: if (event.created < saqlangan.updatedAt) return; // eski event
// Idempotent op 2.14-bob: UPDATE ... SET status='paid' WHERE id=? AND status!='paid'4. Batafsil kod namunalari
Misol 1 — Universal webhook guard (imzo — 2.4, 2.7)
// webhook-signature.guard.ts
@Injectable()
export class WebhookSignatureGuard implements CanActivate {
constructor(private config: ConfigService) {}
canActivate(context: ExecutionContext): boolean {
const req = context.switchToHttp().getRequest<RawBodyRequest<Request>>();
const sig = req.headers["x-signature-256"] as string;
if (!sig || !req.rawBody) throw new UnauthorizedException("Imzo yo'q");
const kutilgan = "sha256=" + createHmac("sha256", this.config.get("WEBHOOK_SECRET"))
.update(req.rawBody).digest("hex");
const a = Buffer.from(sig), b = Buffer.from(kutilgan);
if (a.length !== b.length || !timingSafeEqual(a, b)) { // timing-safe (14)
throw new UnauthorizedException("Imzo noto'g'ri");
}
return true;
}
}Misol 2 — Idempotency service (2.5)
@Injectable()
export class WebhookIdempotencyService {
constructor(@InjectRepository(ProcessedWebhook) private repo: Repository<ProcessedWebhook>) {}
// true = birinchi marta (ishla); false = takror (o'tkazib yubor)
async birinchiMartami(eventId: string): Promise<boolean> {
try {
await this.repo.insert({ eventId }); // UNIQUE — atomik (race-safe)
return true;
} catch (e) {
if (e.code === "23505" || e.code === "ER_DUP_ENTRY") return false; // takror
throw e;
}
}
}Misol 3 — To'liq inbound webhook (4 bosqich — 2.3, 2.6)
@Controller("webhooks")
export class WebhooksController {
constructor(
private idempotency: WebhookIdempotencyService,
@InjectQueue("webhooks") private queue: Queue,
) {}
@Post("stripe")
@HttpCode(200)
@UseGuards(WebhookSignatureGuard) // 1. IMZO (2.4)
async stripe(@Body() event: any) {
// 2. IDEMPOTENCY (2.5)
if (!(await this.idempotency.birinchiMartami(event.id))) {
return { received: true, duplicate: true }; // takror o'tkazib yubor
}
// 3. NAVBATGA (fonda — 2.6)
await this.queue.add("stripe-event", event, { jobId: event.id });
// 4. TEZ 200 (2.6)
return { received: true };
}
}Misol 4 — Webhook worker (fonda ishlov — 8.22)
@Processor("webhooks")
export class WebhookProcessor extends WorkerHost {
constructor(private ordersService: OrdersService, private mailService: MailService) {
super();
}
async process(job: Job) {
const event = job.data;
switch (event.type) {
case "payment_intent.succeeded":
await this.ordersService.tolovTasdiqla(event.data.object.metadata.orderId); // (8.19)
await this.mailService.chekYubor(event.data.object.metadata.orderId);
break;
case "charge.refunded":
await this.ordersService.refundQayta(event.data.object.metadata.orderId);
break;
}
// Xato bo'lsa BullMQ avtomatik retry 8.22-bob tugasa failed (DLQ — 2.9)
}
}Misol 5 — GitHub webhook (real misol — 2.4)
@Post("github")
@HttpCode(200)
async github(
@Req() req: RawBodyRequest<Request>,
@Headers("x-hub-signature-256") sig: string,
@Headers("x-github-event") event: string,
) {
// GitHub HMAC-SHA256 (2.4)
const kutilgan = "sha256=" + createHmac("sha256", this.config.get("GITHUB_WEBHOOK_SECRET"))
.update(req.rawBody).digest("hex");
if (!timingSafeEqual(Buffer.from(sig), Buffer.from(kutilgan))) {
throw new UnauthorizedException();
}
if (event === "push") {
await this.ciQueue.add("deploy", { repo: req.body.repository.name }); // CI/CD (10)
}
return { ok: true };
}Misol 6 — Timestamp tekshirish (replay-attack — 2.10)
// Stripe uslubi — timestamp + imzo (eski webhook qayta yuborilmasin)
function verifyWithTimestamp(rawBody: Buffer, sigHeader: string, secret: string): boolean {
// sigHeader: "t=1614556800,v1=abc123..."
const parts = Object.fromEntries(sigHeader.split(",").map((p) => p.split("=")));
const timestamp = Number(parts.t);
// Replay himoyasi — 5 daqiqadan eski webhook rad (2.10)
if (Math.abs(Date.now() / 1000 - timestamp) > 300) {
throw new UnauthorizedException("Webhook muddati o'tgan (replay?)");
}
const kutilgan = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
return timingSafeEqual(Buffer.from(parts.v1), Buffer.from(kutilgan));
}Misol 7 — Outbound webhook + retry (2.8)
@Injectable()
export class OutboundWebhookService {
constructor(@InjectQueue("outbound-webhooks") private queue: Queue) {}
// Mijozlarga hodisa yuborish (sizning platforma)
async hodisaYubor(hodisa: string, data: any) {
const obunalar = await this.repo.find({ where: { hodisa, faol: true } });
for (const obuna of obunalar) {
await this.queue.add("send", { url: obuna.url, secret: obuna.secret, hodisa, data }, {
attempts: 5, // 5 marta (2.8)
backoff: { type: "exponential", delay: 1000 }, // 1s, 2s, 4s... (BullMQ avtomatik)
});
}
}
}
@Processor("outbound-webhooks")
export class OutboundProcessor extends WorkerHost {
async process(job: Job) {
const { url, secret, hodisa, data } = job.data;
const payload = { hodisa, data, timestamp: Date.now() };
const sig = "sha256=" + createHmac("sha256", secret).update(JSON.stringify(payload)).digest("hex");
await axios.post(url, payload, {
headers: { "x-webhook-signature": sig },
timeout: 10000, // 10s (2.6)
});
// Xato BullMQ retry (backoff); 5 marta tugasa failed (DLQ — 2.9)
}
}Misol 8 — Webhook obuna boshqaruvi (outbound — 2.2)
// Mijozlar webhook URL ro'yxatdan o'tkazadi (sizning API)
@Entity()
export class WebhookSubscription {
@PrimaryGeneratedColumn("uuid") id: string;
@Column() userId: string;
@Column() url: string; // mijoz endpoint
@Column() hodisa: string; // qaysi hodisaga obuna
@Column() secret: string; // imzo uchun (mijozga beriladi)
@Column({ default: true }) faol: boolean;
}
@Controller("webhook-subscriptions")
@UseGuards(JwtAuthGuard)
export class WebhookSubController {
@Post()
yarat(@Body() dto: CreateWebhookDto, @CurrentUser() user) {
const secret = randomBytes(32).toString("hex"); // mijozga beriladi (imzo tekshirish uchun)
return this.service.yarat({ ...dto, userId: user.id, secret });
}
}Misol 9 — Webhook log va monitoring (2.9)
// Har webhook'ni log qilish (debug, audit, replay)
@Entity()
export class WebhookLog {
@PrimaryGeneratedColumn("uuid") id: string;
@Column() provider: string;
@Column() eventId: string;
@Column("jsonb") payload: any;
@Column() status: "received" | "processed" | "failed";
@Column({ nullable: true }) xato: string;
@CreateDateColumn() createdAt: Date;
}
// muammo bo'lsa: log'dan ko'rish, replay qilish (qayta ishlov)
// DLQ monitoring: failed status'larni kuzatish (8.15)Misol 10 — Webhook test (ngrok + mock — 8.11)
// Webhook'ni lokal test (ngrok — lokal URL public)
// terminal: ngrok http 3000 https://abc.ngrok.io gateway'ga shu URL
// Unit test — imzo (8.11)
describe("WebhookSignatureGuard", () => {
it("to'g'ri imzo o'tadi", () => {
const body = Buffer.from(JSON.stringify({ event: "test" }));
const sig = "sha256=" + createHmac("sha256", "secret").update(body).digest("hex");
const ctx = mockContext({ rawBody: body, headers: { "x-signature-256": sig } });
expect(guard.canActivate(ctx)).toBe(true);
});
it("noto'g'ri imzo rad", () => {
const ctx = mockContext({ rawBody: Buffer.from("{}"), headers: { "x-signature-256": "sha256=xato" } });
expect(() => guard.canActivate(ctx)).toThrow(UnauthorizedException);
});
});Misol 11 — Redis idempotency (yuqori hajm — 2.12)
@Injectable()
export class RedisIdempotencyService {
constructor(@InjectRedis() private redis: Redis) {} // ioredis (8.22)
// true = birinchi marta (ishla); false = takror (o'tkazib yubor)
async birinchiMartami(eventId: string): Promise<boolean> {
// SET key "1" NX EX ttl agar mavjud bo'lmasa qo'yadi va "OK" qaytaradi;
// mavjud bo'lsa null (ya'ni takror). Atomik (race-safe — bitta buyruq).
const natija = await this.redis.set(
`webhook:dedup:${eventId}`,
"1",
"NX", // faqat mavjud bo'lmasa
"EX",
3 * 24 * 60 * 60, // TTL 3 kun (provider retry oynasidan uzun — 2.12)
);
return natija === "OK"; // "OK" = yangi; null = takror
}
}
// Kritik (pul) uchun: DB unique (Misol 2) — Redis o'chsa belgi yo'qolmasin (2.12)Misol 12 — Event tartibi: eski hodisani rad qilish (2.13)
// Webhook payload'ida versiya/timestamp bor (Stripe: event.created; boshqalarda: updated_at)
async holatYangila(event: any) {
const buyurtma = await this.repo.findOne({ where: { id: event.data.orderId } });
// ORDERING himoyasi — bu event saqlangandan ESKI bo'lsa e'tiborsiz qoldir (2.13)
// (masalan "succeeded" dan keyin kechikkan "processing" kelib qolsa — bosmasin)
if (buyurtma && event.created * 1000 <= buyurtma.holatVaqti.getTime()) {
return; // eski hodisa — o'tkazib yubor
}
// Yoki — payload'ga ishonmay, haqiqiy holatni provider'dan QAYTA O'QISH (ishonchli — 2.13):
// const haqiqiy = await this.stripe.paymentIntents.retrieve(event.data.object.id);
await this.repo.update(event.data.orderId, {
holat: event.data.holat,
holatVaqti: new Date(event.created * 1000), // eng oxirgi hodisa vaqtini saqla
});
}Misol 13 — Idempotent operatsiya (shartli yangilash — 2.14)
async tolovTasdiqla(orderId: string) {
// NOTO'G'RI (idempotent emas — har takrorda balans o'sadi):
// user.balans += order.summa; await this.userRepo.save(user);
// TO'G'RI — shartli yangilash: faqat hali 'paid' bo'lmagan bo'lsa o'zgartiradi.
// Ikkinchi marta kelsa — affected = 0 (hech narsa o'zgarmaydi — takrorga chidamli — 2.14)
const natija = await this.orderRepo
.createQueryBuilder()
.update(Order)
.set({ status: "paid", paidAt: () => "NOW()" })
.where("id = :id AND status != :paid", { id: orderId, paid: "paid" })
.execute();
if (natija.affected === 0) return; // allaqachon paid yon ta'sir yo'q (email qayta ketmaydi)
// Faqat haqiqatan holat o'zgargandagina yon ta'sir (email, fulfillment — 2.6)
await this.mailService.chekYubor(orderId);
}5. To'g'ri va noto'g'ri holatlar
1) Imzo tekshirmaslik
webhook keldi ishonish (soxta — 2.4)
HMAC + timingSafeEqual2) Sekin handler (DB/email webhook ichida)
webhook'da og'ir ish 10s o'tadi gateway retry (2.6)
navbatga + tez 200 (fonda)3) Idempotency yo'q
takror webhook ikki marta ishlov (2.5)
event ID unique4) Parse qilingan body bilan imzo
json() raw yo'q imzo doim xato (2.7)
rawBody: true5) === bilan imzo solishtirish
sig === kutilgan (timing-attack — 14)
timingSafeEqual6) Payload'ga ko'r-ko'rona ishonish (tartib)
webhook holatini to'g'ridan yozish eski event yangisini bosadi (2.13)
versiya/timestamp taqqoslash yoki provider'dan qayta o'qish7) Idempotent bo'lmagan operatsiya
balans += summa (takrorda o'sadi — 2.14)
shartli yangilash (WHERE status != 'paid') / holat o'rnatish6. Keng tarqalgan xatolar va yechimlari
Xato 1 — Imzo har doim xato
Sababi: raw body emas 2.7-bob. Yechimi: rawBody: true; parse'dan oldin.
Xato 2 — Gateway retry qiladi (takror)
Sababi: sekin javob (>10s) yoki 200 emas 2.6-bob. Yechimi: tez 200 + navbat.
Xato 3 — Ikki marta ishlov
Sababi: idempotency yo'q 2.5-bob. Yechimi: event ID unique.
Xato 4 — Webhook lokal kelmaydi
Sababi: localhost public emas. Yechimi: ngrok (Misol 10).
Xato 5 — Hodisa yo'qoladi
Sababi: xato retry/DLQ yo'q 2.9-bob. Yechimi: navbat retry + dead-letter.
Xato 6 — Thundering herd (retry bo'roni)
Sababi: jitter yo'q 2.8-bob. Yechimi: backoff + jitter.
Xato 7 — Eski hodisa yangisini bosib ketadi
Sababi: tartib kafolatlanmaydi 2.13-bob. Yechimi: timestamp/versiya taqqoslash yoki provider'dan qayta o'qish.
Xato 8 — Takror ishlovda balans/hisob buziladi
Sababi: operatsiya idempotent emas 2.14-bob. Yechimi: shartli yangilash / holat o'rnatish / UPSERT.
7. Integratsiya — bu mavzu stack'ning qayerida uchraydi
- Payment 8.19-bob: to'lov webhook.
- Navbat 8.22-bob: fonda ishlov, retry, Redis (idempotency dedup — 2.12).
- Guard/Middleware 8.6-bob: imzo guard.
- Crypto 5.3-bob: HMAC.
- Xavfsizlik (14): imzo, timestamp, rate limit.
- SMS/Email (5.18, 8.10): status webhook.
- CI/CD (10): GitHub webhook deploy.
- Telegram 8.12-bob: webhook rejim.
8. Eng yaxshi amaliyotlar (best practices)
- Imzo tekshir (HMAC, timingSafeEqual — 2.4) + raw body 2.7-bob.
- Idempotency (event ID unique — 2.5).
- Tez 200 + navbat (fonda — 2.6, 8.22).
- Retry + exponential backoff + jitter (outbound — 2.8).
- Dead-letter queue (yo'qolmaslik — 2.9).
- Timestamp (replay-attack — 2.10).
- HTTPS + rate limit (xavfsizlik — 2.10).
- Webhook log (audit, replay — Misol 9).
- ngrok (lokal test — Misol 10).
- Retriable vs permanent xato farqlash 2.8-bob.
- Idempotency: DB vs Redis to'g'ri tanlash (kritik — DB; hajm — Redis; 2.12).
- Event tartibi (timestamp/versiya yoki provider'dan qayta o'qish — 2.13).
- Idempotent operatsiya dizayni (shartli yangilash / holat o'rnatish — 2.14).
9. Amaliy loyiha: "Ishonchli Webhook Tizimi"
Webhook'ni amalda mustahkamlash.
Maqsad
To'liq webhook tizimi: inbound (imzo, idempotency, fonda) va outbound (retry, DLQ, obuna).
Talablar (requirements)
- Imzo guard: HMAC + timingSafeEqual + raw body (Misol 1, 2.4, 2.7).
- Idempotency: event ID unique (Misol 2, 2.5).
- Inbound: 4 bosqich (imzoidempotencynavbat200) (Misol 3, 2.6).
- Worker: fonda ishlov (Misol 4, 8.22).
- Timestamp: replay himoyasi (Misol 6, 2.10).
- Outbound: retry + backoff + jitter (Misol 7, 2.8).
- DLQ: yo'qolmaslik 2.9-bob.
- Obuna: webhook subscription (Misol 8, 2.2).
- Log: webhook audit (Misol 9).
- Test: imzo + ngrok (Misol 10, 8.11).
- Idempotent operatsiya: shartli yangilash (Misol 13, 2.14).
- Event tartibi: timestamp taqqoslash (Misol 12, 2.13).
Maslahatlar (hint)
- Raw body (2.7, 1-xato).
- Tez 200 + navbat (2.6, 2-xato).
- Idempotency atomik insert (2.5, 3-xato).
- timingSafeEqual (14, 5-holat).
- Jitter (2.8, 6-xato).
"Tayyor" mezonlari (acceptance criteria)
- Imzo guard (HMAC).
- Idempotency.
- Inbound (4 bosqich).
- Worker.
- Timestamp.
- Outbound (retry).
- DLQ.
- Obuna.
- Log.
- Test.
- Idempotent operatsiya (shartli yangilash).
- Event tartibi (timestamp).
Yechim kodi ataylab berilmagan — bu loyihani o'zingiz yozib ko'ring.
10. Xulosa va keyingi bobga ko'prik
Bu bobda webhook'larni to'liq o'rgandik:
- Polling vs webhook 2.1-bob; inbound/outbound 2.2-bob; endpoint 2.3-bob.
- Imzo (HMAC) (xavfsizlik — 2.4); raw body 2.7-bob; idempotency (takror — 2.5; DB vs Redis — 2.12).
- Tez 200 + fonda (navbat — 2.6); retry + backoff + jitter 2.8-bob; dead-letter 2.9-bob; xavfsizlik 2.10-bob.
- Event tartibi (ordering — 2.13); idempotent operatsiya dizayni (shartli yangilash — 2.14).
Keyingi bob — 8.21: Hujjat generatsiya — PDF, Excel, CSV. Webhook'ni bildik; endi yana bir real mavzu — hujjat yaratish (chek/hisob-faktura PDF, hisobot Excel, ma'lumot CSV import-export) — ni o'rganamiz. Har biznesda chek, hisobot, eksport kerak.
Foydalanilgan rasmiy/ishonchli manbalar
- Hooklistener / Hookdeck — Webhooks implementation & infrastructure 2026 (HMAC, idempotency, retry)
- digitalapplied.com — Webhook Reliability 2026: Idempotency & Retry (backoff, jitter, DLQ)
- docs.stripe.com/webhooks (signature, raw body, timestamp tolerance)
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!