IlmHamroh
Dasturlash kitobi (eski nashr)/8-QISM — NestJS20/30-dars22 daqiqa
Mundarija (42)

Webhook va idempotency — HMAC imzo tekshiruvi, NestJS'da (8.20-bob)

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 tekshirish (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 yozilsa ham — bir xil natija)
  ✅ UPSERT (bor bo'lsa yangilash, yo'q bo'lsa yaratish — takrorga chidamli)
  ✅ "agar hali paid emas bo'lsa → paid qilish" (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 qilish.


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 saqlash
  });
}

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-bob
✅ navbatga + tez 200 (fonda)

3) Idempotency yo'q

text
❌ takror webhook → ikki marta ishlov 2.5-bob
✅ event ID unique

4) Parse qilingan body bilan imzo

text
❌ json() → raw yo'q → imzo doim xato 2.7-bob
✅ 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-bob
✅ 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 tekshirish (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 (imzo→idempotency→navbat→200) (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)
Ulashish:Telegram'da

O'qiganingizni mustahkamlang

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Webhook va idempotency — HMAC imzo tekshiruvi, NestJS'da (8.20-bob) — IlmHamroh