สรุป
ออกแบบ Webhook ที่เชื่อถือได้ด้วยการลองใหม่แบบ exponential backoff (5-10 ครั้ง), คีย์ idempotency, การยืนยันลายเซ็น HMAC และการหมดเวลา 5 วินาที ส่งคืน 2xx ทันที ประมวลผลแบบอะซิงโครนัส Modern PetstoreAPI นำ Webhook มาใช้สำหรับการอัปเดตคำสั่งซื้อ การรับเลี้ยงสัตว์เลี้ยง และการแจ้งเตือนการชำระเงิน พร้อมการลองใหม่และความปลอดภัยเต็มรูปแบบ
บทนำ
คุณส่ง Webhook เพื่อแจ้งให้ลูกค้าทราบว่าสัตว์เลี้ยงของพวกเขาถูกรับเลี้ยงไปแล้ว แต่เซิร์ฟเวอร์ของลูกค้าขัดข้อง Webhook ของคุณล้มเหลว คุณควรลองใหม่หรือไม่? กี่ครั้ง? จะเกิดอะไรขึ้นหากลูกค้าได้รับ Webhook สองครั้งและเรียกเก็บเงินลูกค้าสองครั้ง?
Webhook คือการเรียกกลับ HTTP ที่ผลักดันเหตุการณ์ไปยัง URL ของลูกค้า สิ่งเหล่านี้ดูง่ายในทางทฤษฎี แต่ซับซ้อนในทางปฏิบัติ เครือข่ายล้มเหลว เซิร์ฟเวอร์ขัดข้อง และลูกค้ามีข้อบกพร่อง Webhook ที่ใช้งานจริงจำเป็นต้องมีตรรกะการลองใหม่ (retry logic), idempotency, ความปลอดภัย และการตรวจสอบ
Modern PetstoreAPI นำ Webhook ที่พร้อมสำหรับการใช้งานจริงมาใช้สำหรับการอัปเดตคำสั่งซื้อ การรับเลี้ยงสัตว์เลี้ยง และการแจ้งเตือนการชำระเงิน Webhook ทุกตัวมีตรรกะการลองใหม่ การยืนยันลายเซ็น และ idempotency
ในคู่มือนี้ คุณจะได้เรียนรู้วิธีการออกแบบ Webhook ที่เชื่อถือได้โดยใช้รูปแบบของ Modern PetstoreAPI
พื้นฐาน Webhook
Webhook คือคำขอ HTTP POST ที่ส่งไปยัง URL ที่ลูกค้าให้ไว้เมื่อเกิดเหตุการณ์ขึ้น
Webhook ทำงานอย่างไร
1. ลูกค้าลงทะเบียน URL ของ Webhook:
POST /webhooks
{
"url": "https://client.com/webhooks/petstore",
"events": ["pet.adopted", "order.completed"]
}
2. เกิดเหตุการณ์ขึ้น (สัตว์เลี้ยงถูกรับเลี้ยง)
3. เซิร์ฟเวอร์ส่ง Webhook:
POST https://client.com/webhooks/petstore
Content-Type: application/json
X-Webhook-Signature: sha256=abc123...
{
"event": "pet.adopted",
"data": {
"petId": "019b4132",
"userId": "user-456",
"timestamp": "2026-03-13T10:30:00Z"
}
}
4. ลูกค้าตอบกลับ:
200 OK
ปัญหาความน่าเชื่อถือ
Webhook อาจล้มเหลวได้หลายสาเหตุ:
- เซิร์ฟเวอร์ของลูกค้าขัดข้อง
- เครือข่ายหมดเวลา
- ลูกค้าส่งข้อผิดพลาด 500 กลับมา
- ลูกค้าทำงานช้า (ใช้เวลา 30 วินาที)
- ลูกค้าได้รับ Webhook แต่ขัดข้องก่อนประมวลผล
หากไม่มีตรรกะการลองใหม่ เหตุการณ์จะสูญหายไป หากไม่มี idempotency การส่ง Webhook ซ้ำกันจะทำให้เกิดการดำเนินการซ้ำซ้อน
ตรรกะการลองใหม่ด้วย Exponential Backoff
ลองส่ง Webhook ที่ล้มเหลวซ้ำอีกครั้งด้วยการหน่วงเวลาที่เพิ่มขึ้น
กลยุทธ์ Exponential Backoff
Attempt 1: ทันที
Attempt 2: 1 วินาทีต่อมา
Attempt 3: 2 วินาทีต่อมา
Attempt 4: 4 วินาทีต่อมา
Attempt 5: 8 วินาทีต่อมา
Attempt 6: 16 วินาทีต่อมา
ทำไมต้องเป็นแบบ exponential? หากลูกค้าออฟไลน์ การลองใหม่ซ้ำๆ จะไม่ช่วยอะไร Exponential backoff ให้เวลาในการกู้คืนระบบ
การนำไปใช้งาน
async function sendWebhook(url, payload, attempt = 1, maxAttempts = 6) {
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature': generateSignature(payload)
},
body: JSON.stringify(payload),
timeout: 5000 // 5 second timeout
});
if (response.ok) {
return { success: true, attempt };
}
// Retry on 5xx errors
if (response.status >= 500 && attempt < maxAttempts) {
const delay = Math.pow(2, attempt - 1) * 1000;
await sleep(delay);
return sendWebhook(url, payload, attempt + 1, maxAttempts);
}
// Don't retry 4xx errors (client error)
return { success: false, status: response.status };
} catch (error) {
// Network error or timeout - retry
if (attempt < maxAttempts) {
const delay = Math.pow(2, attempt - 1) * 1000;
await sleep(delay);
return sendWebhook(url, payload, attempt + 1, maxAttempts);
}
return { success: false, error: error.message };
}
}
ควรลองใหม่เมื่อใด
ลองใหม่เมื่อ:
- ข้อผิดพลาดเซิร์ฟเวอร์ 5xx (500, 502, 503, 504)
- เครือข่ายหมดเวลา
- การเชื่อมต่อถูกปฏิเสธ
- ความล้มเหลวของ DNS
ไม่ต้องลองใหม่เมื่อ:
- ข้อผิดพลาดไคลเอ็นต์ 4xx (400, 401, 404) - ลูกค้าจะไม่แก้ไขสิ่งเหล่านี้
- 2xx สำเร็จ - สำเร็จไปแล้ว
Dead Letter Queue
หลังจากลองใหม่ครบจำนวนสูงสุดแล้ว ให้ย้าย Webhook ที่ล้มเหลวไปยัง Dead Letter Queue เพื่อตรวจสอบด้วยตนเอง:
if (!result.success) {
await deadLetterQueue.add({
url,
payload,
attempts: maxAttempts,
lastError: result.error,
timestamp: new Date()
});
}
Idempotency เพื่อป้องกันการทำซ้ำ
ลูกค้าอาจได้รับ Webhook เดียวกันหลายครั้ง Idempotency ช่วยป้องกันการประมวลผลซ้ำซ้อน
คีย์ Idempotency
รวม ID ที่ไม่ซ้ำกันกับ Webhook แต่ละตัว:
{
"id": "webhook_019b4132",
"event": "pet.adopted",
"data": {...}
}
ลูกค้าจัดเก็บ ID ที่ประมวลผลแล้ว:
app.post('/webhooks/petstore', async (req, res) => {
const webhookId = req.body.id;
// Check if already processed
const processed = await db.webhooks.findOne({ id: webhookId });
if (processed) {
return res.status(200).json({ message: 'Already processed' });
}
// Process webhook
await processPetAdoption(req.body.data);
// Mark as processed
await db.webhooks.insert({ id: webhookId, processedAt: new Date() });
res.status(200).json({ message: 'Processed' });
});
การดำเนินการแบบ Idempotent
ออกแบบการดำเนินการให้เป็นแบบ idempotent:
ไม่ดี (ไม่เป็น idempotent):
// Charging twice causes double charge
await chargeCustomer(userId, amount);
ดี (เป็น idempotent):
// Charging with idempotency key prevents double charge
await chargeCustomer(userId, amount, { idempotencyKey: webhookId });
การยืนยันลายเซ็นเพื่อความปลอดภัย
ตรวจสอบว่า Webhook มาจาก API ของคุณ ไม่ใช่จากผู้โจมตี
ลายเซ็น HMAC
สร้างลายเซ็นโดยใช้ shared secret:
// Server generates signature
const crypto = require('crypto');
function generateSignature(payload, secret) {
const hmac = crypto.createHmac('sha256', secret);
hmac.update(JSON.stringify(payload));
return hmac.digest('hex');
}
// Include in header
headers['X-Webhook-Signature'] = `sha256=${generateSignature(payload, webhookSecret)}`;
ไคลเอ็นต์ยืนยันลายเซ็น:
function verifySignature(payload, signature, secret) {
const expected = generateSignature(payload, secret);
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(`sha256=${expected}`)
);
}
app.post('/webhooks/petstore', (req, res) => {
const signature = req.headers['x-webhook-signature'];
if (!verifySignature(req.body, signature, WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process webhook
...
});
การตรวจสอบเวลา
รวม timestamp เพื่อป้องกันการโจมตีแบบ replay:
{
"id": "webhook_019b4132",
"timestamp": "2026-03-13T10:30:00Z",
"event": "pet.adopted",
"data": {...}
}
ปฏิเสธ Webhook เก่า:
const webhookAge = Date.now() - new Date(req.body.timestamp);
if (webhookAge > 5 * 60 * 1000) { // 5 minutes
return res.status(400).json({ error: 'Webhook too old' });
}
การจัดการการหมดเวลา
กำหนด aggressive timeouts เพื่อป้องกันไม่ให้ไคลเอ็นต์ที่ทำงานช้าทำให้ระบบของคุณหยุดชะงัก
หมดเวลา 5 วินาที
const response = await fetch(url, {
method: 'POST',
body: JSON.stringify(payload),
timeout: 5000 // 5 seconds
});
ทำไมต้อง 5 วินาที? Webhook ควรส่งคืนทันที หากไคลเอ็นต์ใช้เวลานานกว่านั้น แสดงว่าพวกเขากำลังประมวลผลแบบ synchronous (รูปแบบที่ไม่ถูกต้อง)
รูปแบบการประมวลผลแบบ Asynchronous
ไม่ดี (synchronous):
app.post('/webhooks/petstore', async (req, res) => {
// This takes 30 seconds - webhook will timeout
await processOrder(req.body.data);
await sendEmail(req.body.data);
await updateInventory(req.body.data);
res.status(200).json({ message: 'Processed' });
});
ดี (asynchronous):
app.post('/webhooks/petstore', async (req, res) => {
// Return immediately
res.status(200).json({ message: 'Received' });
// Process asynchronously
queue.add('process-webhook', req.body);
});
Modern PetstoreAPI นำ Webhook ไปใช้งานอย่างไร
Modern PetstoreAPI นำ Webhook ที่พร้อมสำหรับการใช้งานจริงมาใช้
เหตุการณ์ Webhook
pet.adopted - สัตว์เลี้ยงถูกรับเลี้ยง
pet.status_changed - สถานะสัตว์เลี้ยงเปลี่ยนไป
order.created - สร้างคำสั่งซื้อ
order.completed - คำสั่งซื้อเสร็จสมบูรณ์
payment.succeeded - การชำระเงินสำเร็จ
payment.failed - การชำระเงินล้มเหลว
เพย์โหลด Webhook
{
"id": "webhook_019b4132-70aa-764f-b315-e2803d882a24",
"event": "pet.adopted",
"timestamp": "2026-03-13T10:30:00Z",
"data": {
"petId": "019b4132-70aa-764f-b315-e2803d882a24",
"userId": "user-456",
"orderId": "order-789",
"adoptionDate": "2026-03-13"
},
"apiVersion": "v1"
}
การกำหนดค่าการลองใหม่
- จำนวนครั้งที่ลองใหม่สูงสุด: 10 ครั้ง
- Backoff: Exponential (1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s)
- ระยะเวลาการลองใหม่ทั้งหมด: ประมาณ 17 นาที
- Dead Letter Queue หลังจากลองใหม่ครบจำนวนสูงสุด
ความปลอดภัย
- ลายเซ็น HMAC-SHA256 ในส่วนหัว
X-Webhook-Signature - การตรวจสอบเวลา (ปฏิเสธ Webhook ที่มีอายุมากกว่า 5 นาที)
- จำเป็นต้องใช้ HTTPS สำหรับ URL ของ Webhook
การทดสอบ Webhook ด้วย Apidog
Apidog รองรับการทดสอบ Webhook
ทดสอบการส่ง Webhook
- สร้าง mock webhook endpoint ใน Apidog
- ลงทะเบียน endpoint กับ PetstoreAPI
- ทริกเกอร์เหตุการณ์ (รับเลี้ยงสัตว์เลี้ยง)
- ตรวจสอบว่าได้รับ Webhook
- ตรวจสอบรูปแบบเพย์โหลด
ทดสอบการยืนยันลายเซ็น
// Apidog test script
const signature = pm.request.headers.get('X-Webhook-Signature');
const payload = pm.request.body.raw;
const secret = pm.environment.get('WEBHOOK_SECRET');
const expected = generateSignature(payload, secret);
pm.test('Signature valid', () => {
pm.expect(signature).to.equal(`sha256=${expected}`);
});
ทดสอบตรรกะการลองใหม่
- ส่งคืนข้อผิดพลาด 500 จาก mock endpoint
- ตรวจสอบการลองใหม่ด้วย exponential backoff
- ตรวจสอบ dead letter queue หลังจากลองใหม่ครบจำนวนสูงสุด
ทดสอบ Idempotency
- รับ Webhook
- ส่งคืน 200
- รับ Webhook เดิมอีกครั้ง (จำลองการลองใหม่)
- ตรวจสอบว่าไม่มีการประมวลผลซ้ำซ้อน
บทสรุป
Webhook ที่เชื่อถือได้ต้องการ:
- การลองใหม่แบบ exponential backoff (5-10 ครั้ง)
- คีย์ Idempotency เพื่อป้องกันการทำซ้ำ
- การยืนยันลายเซ็น HMAC เพื่อความปลอดภัย
- การหมดเวลา 5 วินาที
- การประมวลผลแบบอะซิงโครนัสฝั่งไคลเอ็นต์
- Dead Letter Queue สำหรับ Webhook ที่ล้มเหลว
Modern PetstoreAPI นำรูปแบบทั้งหมดเหล่านี้ไปใช้ ตรวจสอบ เอกสารประกอบ Webhook สำหรับตัวอย่างที่สมบูรณ์
ทดสอบ Webhook ของคุณด้วย Apidog เพื่อตรวจสอบตรรกะการลองใหม่ ลายเซ็น และ idempotency ก่อนนำไปใช้งานจริง
คำถามที่พบบ่อย
Webhook ควรมียอดการลองใหม่กี่ครั้ง?
5-10 ครั้งด้วย exponential backoff สิ่งนี้ครอบคลุมการหยุดทำงานชั่วคราว (5-17 นาที) โดยไม่ทำให้ลูกค้ามีภาระมากเกินไป
Webhook ควรถอนการลองใหม่เมื่อเกิดข้อผิดพลาด 4xx หรือไม่?
ไม่ ข้อผิดพลาด 4xx บ่งชี้ปัญหาของลูกค้า (URL ไม่ถูกต้อง, การยืนยันตัวตนล้มเหลว) การลองใหม่จะไม่แก้ไขสิ่งเหล่านี้ ลองใหม่เฉพาะข้อผิดพลาด 5xx และความล้มเหลวของเครือข่ายเท่านั้น
Webhook ควรหมดเวลาภายในระยะเวลาเท่าใด?
สูงสุด 5 วินาที ไคลเอ็นต์ควรส่งคืน 200 ทันทีและประมวลผลแบบอะซิงโครนัส การหมดเวลาที่นานกว่านั้นบ่งชี้ว่าไคลเอ็นต์กำลังประมวลผลแบบ synchronous
จะเกิดอะไรขึ้นหากไคลเอ็นต์ไม่ตอบสนองต่อ Webhook เลย?
หลังจากลองใหม่ครบจำนวนสูงสุด ให้ย้ายไปยัง dead letter queue แจ้งเตือนลูกค้าทางอีเมล พิจารณาปิดใช้งาน Webhook สำหรับลูกค้ารายนั้นหลังจากความล้มเหลวซ้ำๆ
URL ของ Webhook ควรเป็น HTTPS หรือไม่?
ใช่ ต้องใช้ HTTPS เสมอ Webhook แบบ HTTP อาจถูกดักจับและแก้ไขได้ Modern PetstoreAPI ปฏิเสธ URL ของ Webhook แบบ HTTP
คุณจะป้องกันการโจมตีแบบ Replay ได้อย่างไร?
รวม timestamp ในเพย์โหลดและปฏิเสธ Webhook ที่มีอายุมากกว่า 5 นาที ร่วมกับการยืนยันลายเซ็น
ไคลเอ็นต์สามารถขอให้ส่ง Webhook ซ้ำได้หรือไม่?
ใช่ Modern PetstoreAPI มี endpoint สำหรับการส่ง Webhook ที่เฉพาะเจาะจงซ้ำ: POST /webhooks/{id}/redeliver
คุณทดสอบ Webhook ในเครื่องได้อย่างไร?
ใช้เครื่องมืออย่าง ngrok เพื่อเปิดเผย localhost สู่สาธารณะ หรือใช้ mock server ของ Apidog เพื่อจำลอง Webhook endpoint ระหว่างการพัฒนา
