บทความทั้งหมด

รับ Webhook ซ้ำและล่าช้าอย่างไรให้ระบบไม่พลาด

อ่าน 2 นาที

ไม่มีรูปภาพ
รับ Webhook ซ้ำและล่าช้าอย่างไรให้ระบบไม่พลาด

สมมติว่า Webhook ของคุณล่มไปสิบนาทีตอนมีลูกค้าจ่ายเงินพอดี เมื่อระบบกลับมา คุณจะได้เหตุการณ์ที่มาช้า บางตัวมาซ้ำ และบางตัวมาสลับลำดับ ถ้า handler ไม่ได้ออกแบบมารองรับ ออเดอร์อาจถูกอนุมัติสองครั้งหรือค้างอยู่ตลอดไป บทความนี้อธิบายกลไกการส่งซ้ำของ PPay ตามคู่มือ และวิธีเขียนฝั่งรับให้ทนทาน

เข้าใจการส่งแบบ at-least-once

คู่มือระบุว่าระบบรับประกัน "ส่งอย่างน้อยหนึ่งครั้ง" ความหมายคือ ถ้าคุณตอบ 2xx ช้าเกินกำหนดหรือเครือข่ายหลุด อาจได้รับเหตุการณ์เดิมซ้ำ นี่ไม่ใช่ข้อบกพร่อง แต่เป็นการแลกเปลี่ยนที่ตั้งใจ เพราะระบบเลือกว่าส่งซ้ำดีกว่าทำให้เหตุการณ์หายไป หน้าที่ของฝั่งรับคือทำให้ "รับกี่ครั้งก็ได้ผลลัพธ์เท่ากัน"

ตารางการส่งซ้ำ

ครั้งที่ล้มเหลวส่งซ้ำอีกครั้งใน
11 นาที
25 นาที
330 นาที
42 ชั่วโมง
56 ชั่วโมง
6เลิกส่งอัตโนมัติ (สถานะล้มเหลว) กดส่งซ้ำเองได้ในหน้า API สำหรับนักพัฒนา

จากตารางนี้ หากเซิร์ฟเวอร์คุณล่มนานกว่าประมาณ 8 ชั่วโมงกว่า ๆ นับรวมช่วงรอ เหตุการณ์ที่ล้มเหลวทุกครั้งจะถูกหยุดส่ง ดังนั้นหลังกู้ระบบ ควรมีขั้นตอน "กู้คืน" ไม่ใช่รอเฉย ๆ คือกดส่งซ้ำจากหน้าจัดการ หรือดึงสถานะของงานที่ค้างด้วย GET /api/v1/jobs/{jobId} (ผลงานคิวเก็บ 30 วัน ส่วนประวัติ Webhook เก็บ 14 วัน)

เงื่อนไขที่นับว่าส่งล้มเหลวคือไม่ได้ 2xx ภายใน 8 วินาที รวมถึง 3xx เพราะระบบไม่ตามการ redirect ดังนั้นตรวจให้แน่ใจว่า URL ที่ลงทะเบียนไม่ถูก redirect เช่นจาก http ไป https หรือเพิ่มเครื่องหมายทับท้าย

หลักการ idempotent handler

ใช้ฟิลด์ id ของเหตุการณ์ (หรือ data.jobId) เป็นกุญแจกันซ้ำ แนวทางที่ปลอดภัยที่สุดคือใช้ฐานข้อมูลที่มี unique constraint

// pseudo-code
app.post("/webhooks/ppay", express.raw({ type: "application/json" }), async (req, res) => {
  // 1) verify signature first (see the signature article)
  const event = JSON.parse(req.body.toString("utf8"));

  // 2) record the event id; the unique constraint rejects duplicates
  const first = await db.insertIgnore("webhook_events", { id: event.id });

  // 3) acknowledge quickly
  res.sendStatus(200);

  // 4) do the work only the first time
  if (first) await queue.push(event);
});

ข้อสังเกตคือ ตอบ 200 ทั้งกรณีใหม่และกรณีซ้ำ เพราะถ้าตอบ error กับเหตุการณ์ซ้ำ ระบบจะส่งซ้ำไปเรื่อย ๆ ส่วนงานจริงให้ใส่คิวภายในของคุณ เพื่อให้ตอบกลับทันก่อน 8 วินาที

ตอบเร็วแล้วทำงานทีหลัง

อย่าทำงานหนักก่อนตอบ เช่นส่งอีเมล เรียก API ภายนอก หรือพิมพ์ใบเสร็จ ถ้างานเหล่านั้นช้า คุณจะเกิน 8 วินาที ระบบมองว่าล้มเหลวแล้วส่งซ้ำ ทำให้เกิดงานซ้ำซ้อน เขียนเหตุการณ์ลงตารางหรือคิวก่อน ตอบ 200 แล้วให้ worker แยกประมวลผลต่อ ถ้า worker พลาดก็ลองใหม่ได้โดยไม่ต้องพึ่งการส่งซ้ำจาก PPay

เหตุการณ์ที่มาช้าหรือสลับลำดับ

อย่าสมมติว่าเหตุการณ์มาตามลำดับเวลา ให้ออกแบบให้ปลอดภัยแม้ลำดับสลับ

  • ใช้ data.reference ที่คุณส่งไปตอน /verify/async จับคู่ออเดอร์ ไม่ใช้ลำดับที่มาถึง
  • ก่อนอนุมัติ ให้ตรวจสถานะปัจจุบันของออเดอร์ ถ้ายกเลิกหรือหมดอายุไปแล้ว ให้ส่งเข้ากระบวนการตรวจด้วยคนหรือกระบวนการคืนเงิน ดูแนวทางที่ ขั้นตอนคืนเงินของร้านออนไลน์
  • ถ้าได้เหตุการณ์เก่ามาก ให้ตรวจเวลา createdAt ร่วมกับเวลาของออเดอร์ เพื่อรู้ว่าเป็นของช่วงเวลาใด
  • สำหรับผลที่ผิดปกติ ใช้ GET /api/v1/jobs/{jobId} ยืนยันสถานะล่าสุดกับต้นทางได้

ตรวจสอบและเฝ้าระวัง

  • บันทึก X-PPay-Delivery และ X-PPay-Attempt ลง log เพื่อรู้ว่าเหตุการณ์ใดถูกส่งซ้ำกี่ครั้ง
  • ตั้งการแจ้งเตือนเมื่ออัตราการตอบที่ไม่ใช่ 2xx สูงผิดปกติ
  • มีรายงานรายวันเทียบออเดอร์ที่รอชำระเกินเวลากับสถานะงานฝั่ง PPay ดู การกระทบยอดรายวัน

เช็กลิสต์

  1. ตรวจลายเซ็นก่อนเสมอ
  2. กันซ้ำด้วย id ที่มี unique constraint
  3. ตอบ 2xx ภายใน 8 วินาที งานหนักไปคิว
  4. URL ไม่ redirect
  5. มีขั้นตอนกู้คืนหลังระบบล่ม
  6. ออกแบบโดยไม่พึ่งลำดับของเหตุการณ์

อ่านเรื่องลายเซ็นต่อที่ การตรวจสอบ webhook และดูรายการเหตุการณ์ทั้งหมดใน คู่มือ API

ลองตรวจสลิปอัตโนมัติกับร้านของคุณ

สมัครแล้วเริ่มใช้งานได้ภายในไม่กี่นาที

บทความอื่น ๆ