WisarWisar
Dasturlash kitobi/8-QISM — NestJS22 daqiqa

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

text
  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

text
  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 berish

Inbound (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)

typescript
@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)

typescript
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)

typescript
// 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 + insert o'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)

typescript
@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)

typescript
// 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) yoki express.raw() (muayyan route). Imzo tekshirishdan keyin parse. Bu xato — webhook integratsiyasida №1 muammo (imzo doim fail).

2.8. Retry, exponential backoff, jitter (outbound)

typescript
// 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)

text
  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)

text
   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)

text
  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)

text
  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 UPDATE yoki insert bir tranzaksiyada — atomik). Redis SET 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)

text
  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'lmaslik

Event 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

text
  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 himoya

Idempotent 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

typescript
// 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)

typescript
// 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)

typescript
@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)

typescript
@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)

typescript
@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)

typescript
@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)

typescript
// 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)

typescript
@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)

typescript
// 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)

typescript
// 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)

typescript
// 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)

typescript
@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)

typescript
// 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)

typescript
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

text
 webhook keldi  ishonish (soxta — 2.4)
 HMAC + timingSafeEqual

2) Sekin handler (DB/email webhook ichida)

text
 webhook'da og'ir ish  10s o'tadi  gateway retry (2.6)
 navbatga + tez 200 (fonda)

3) Idempotency yo'q

text
 takror webhook  ikki marta ishlov (2.5)
 event ID unique

4) Parse qilingan body bilan imzo

text
 json()  raw yo'q  imzo doim xato (2.7)
 rawBody: true

5) === bilan imzo solishtirish

text
 sig === kutilgan (timing-attack — 14)
 timingSafeEqual

6) Payload'ga ko'r-ko'rona ishonish (tartib)

text
 webhook holatini to'g'ridan yozish  eski event yangisini bosadi (2.13)
 versiya/timestamp taqqoslash yoki provider'dan qayta o'qish

7) Idempotent bo'lmagan operatsiya

text
 balans += summa (takrorda o'sadi — 2.14)
 shartli yangilash (WHERE status != 'paid') / holat o'rnatish

6. 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)

  1. Imzo guard: HMAC + timingSafeEqual + raw body (Misol 1, 2.4, 2.7).
  2. Idempotency: event ID unique (Misol 2, 2.5).
  3. Inbound: 4 bosqich (imzoidempotencynavbat200) (Misol 3, 2.6).
  4. Worker: fonda ishlov (Misol 4, 8.22).
  5. Timestamp: replay himoyasi (Misol 6, 2.10).
  6. Outbound: retry + backoff + jitter (Misol 7, 2.8).
  7. DLQ: yo'qolmaslik 2.9-bob.
  8. Obuna: webhook subscription (Misol 8, 2.2).
  9. Log: webhook audit (Misol 9).
  10. Test: imzo + ngrok (Misol 10, 8.11).
  11. Idempotent operatsiya: shartli yangilash (Misol 13, 2.14).
  12. 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!
8.20-bob: Webhook'lar va idempotency — Wisar