TL;DR
Thiết kế webhook đáng tin cậy với cơ chế thử lại lũy thừa (5-10 lần), khóa bất biến, xác minh chữ ký HMAC và thời gian chờ 5 giây. Trả về 2xx ngay lập tức, xử lý không đồng bộ. PetstoreAPI hiện đại triển khai webhook cho các cập nhật đơn hàng, nhận nuôi thú cưng và thông báo thanh toán với cơ chế thử lại và bảo mật đầy đủ.
Giới thiệu
Bạn gửi một webhook để thông báo cho khách hàng rằng thú cưng của họ đã được nhận nuôi. Máy chủ của khách hàng đang bị lỗi. Webhook của bạn thất bại. Bạn có thử lại không? Bao nhiêu lần? Điều gì sẽ xảy ra nếu khách hàng nhận được webhook hai lần và bị tính phí hai lần?
Webhook là các callback HTTP đẩy sự kiện đến các URL của khách hàng. Chúng đơn giản về mặt lý thuyết nhưng phức tạp trong thực tế. Mạng bị lỗi, máy chủ gặp sự cố và khách hàng có lỗi. Webhook trong môi trường sản xuất cần logic thử lại, tính bất biến, bảo mật và giám sát.
PetstoreAPI hiện đại triển khai webhook sẵn sàng cho sản xuất để cập nhật đơn hàng, nhận nuôi thú cưng và thông báo thanh toán. Mỗi webhook bao gồm logic thử lại, xác minh chữ ký và tính bất biến.
Trong hướng dẫn này, bạn sẽ tìm hiểu cách thiết kế webhook đáng tin cậy bằng cách sử dụng các mẫu của PetstoreAPI hiện đại.
Những điều cơ bản về Webhook
Webhook là các yêu cầu HTTP POST được gửi đến các URL do khách hàng cung cấp khi sự kiện xảy ra.
Cách Webhook hoạt động
1. Khách hàng đăng ký URL webhook:
POST /webhooks
{
"url": "https://client.com/webhooks/petstore",
"events": ["pet.adopted", "order.completed"]
}
2. Sự kiện xảy ra (thú cưng được nhận nuôi)
3. Máy chủ gửi 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. Khách hàng phản hồi:
200 OK
Vấn đề về độ tin cậy
Webhook có thể thất bại vì nhiều lý do:
- Máy chủ khách hàng đang bị lỗi
- Hết thời gian chờ mạng
- Khách hàng trả về lỗi 500
- Khách hàng phản hồi chậm (mất 30 giây)
- Khách hàng nhận được webhook nhưng gặp sự cố trước khi xử lý
Nếu không có logic thử lại, các sự kiện sẽ bị mất. Nếu không có tính bất biến, webhook trùng lặp sẽ gây ra các hành động trùng lặp.
Logic thử lại với cơ chế backoff lũy thừa
Thử lại các webhook thất bại với độ trễ tăng dần.
Chiến lược Backoff lũy thừa
Lần thử 1: Ngay lập tức
Lần thử 2: Sau 1 giây
Lần thử 3: Sau 2 giây
Lần thử 4: Sau 4 giây
Lần thử 5: Sau 8 giây
Lần thử 6: Sau 16 giây
Tại sao là lũy thừa? Nếu khách hàng bị lỗi, việc thử lại liên tục sẽ không giúp ích gì. Backoff lũy thừa cho phép thời gian để phục hồi.
Triển khai
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 };
}
// Thử lại khi có lỗi 5xx
if (response.status >= 500 && attempt < maxAttempts) {
const delay = Math.pow(2, attempt - 1) * 1000;
await sleep(delay);
return sendWebhook(url, payload, attempt + 1, maxAttempts);
}
// Không thử lại khi có lỗi 4xx (lỗi phía client)
return { success: false, status: response.status };
} catch (error) {
// Lỗi mạng hoặc hết thời gian chờ - thử lại
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 };
}
}
Khi nào nên thử lại
Thử lại khi:
- Lỗi máy chủ 5xx (500, 502, 503, 504)
- Hết thời gian chờ mạng
- Kết nối bị từ chối
- Lỗi DNS
Không thử lại khi:
- Lỗi khách hàng 4xx (400, 401, 404) - khách hàng sẽ không tự sửa được
- Thành công 2xx - đã thành công
Hàng đợi thư chết (Dead Letter Queue)
Sau khi đạt số lần thử lại tối đa, di chuyển các webhook thất bại vào hàng đợi thư chết để xem xét thủ công:
if (!result.success) {
await deadLetterQueue.add({
url,
payload,
attempts: maxAttempts,
lastError: result.error,
timestamp: new Date()
});
}
Tính bất biến để ngăn chặn trùng lặp
Khách hàng có thể nhận được cùng một webhook nhiều lần. Tính bất biến ngăn chặn việc xử lý trùng lặp.
Khóa bất biến
Bao gồm một ID duy nhất với mỗi webhook:
{
"id": "webhook_019b4132",
"event": "pet.adopted",
"data": {...}
}
Khách hàng lưu trữ các ID đã xử lý:
app.post('/webhooks/petstore', async (req, res) => {
const webhookId = req.body.id;
// Kiểm tra xem đã được xử lý chưa
const processed = await db.webhooks.findOne({ id: webhookId });
if (processed) {
return res.status(200).json({ message: 'Đã được xử lý' });
}
// Xử lý webhook
await processPetAdoption(req.body.data);
// Đánh dấu là đã xử lý
await db.webhooks.insert({ id: webhookId, processedAt: new Date() });
res.status(200).json({ message: 'Đã xử lý' });
});
Các hoạt động bất biến
Thiết kế các hoạt động để có tính bất biến:
Không tốt (không bất biến):
// Tính phí hai lần gây ra việc tính phí gấp đôi
await chargeCustomer(userId, amount);
Tốt (bất biến):
// Tính phí với khóa bất biến ngăn chặn việc tính phí gấp đôi
await chargeCustomer(userId, amount, { idempotencyKey: webhookId });
Xác minh chữ ký để bảo mật
Xác minh webhook đến từ API của bạn, không phải từ kẻ tấn công.
Chữ ký HMAC
Tạo chữ ký bằng cách sử dụng khóa bí mật chia sẻ:
// Máy chủ tạo chữ ký
const crypto = require('crypto');
function generateSignature(payload, secret) {
const hmac = crypto.createHmac('sha256', secret);
hmac.update(JSON.stringify(payload));
return hmac.digest('hex');
}
// Bao gồm trong header
headers['X-Webhook-Signature'] = `sha256=${generateSignature(payload, webhookSecret)}`;
Khách hàng xác minh chữ ký:
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: 'Chữ ký không hợp lệ' });
}
// Xử lý webhook
...
});
Xác thực dấu thời gian
Bao gồm dấu thời gian để ngăn chặn các cuộc tấn công phát lại:
{
"id": "webhook_019b4132",
"timestamp": "2026-03-13T10:30:00Z",
"event": "pet.adopted",
"data": {...}
}
Từ chối các webhook cũ:
const webhookAge = Date.now() - new Date(req.body.timestamp);
if (webhookAge > 5 * 60 * 1000) { // 5 phút
return res.status(400).json({ error: 'Webhook quá cũ' });
}
Xử lý thời gian chờ
Đặt thời gian chờ nghiêm ngặt để ngăn chặn các khách hàng chậm làm tắc nghẽn hệ thống của bạn.
Thời gian chờ 5 giây
const response = await fetch(url, {
method: 'POST',
body: JSON.stringify(payload),
timeout: 5000 // 5 giây
});
Tại sao là 5 giây? Webhook nên trả về ngay lập tức. Nếu một khách hàng mất nhiều thời gian hơn, họ đang thực hiện xử lý đồng bộ (mẫu sai).
Mẫu xử lý không đồng bộ
Không tốt (đồng bộ):
app.post('/webhooks/petstore', async (req, res) => {
// Việc này mất 30 giây - webhook sẽ hết thời gian chờ
await processOrder(req.body.data);
await sendEmail(req.body.data);
await updateInventory(req.body.data);
res.status(200).json({ message: 'Đã xử lý' });
});
Tốt (không đồng bộ):
app.post('/webhooks/petstore', async (req, res) => {
// Trả về ngay lập tức
res.status(200).json({ message: 'Đã nhận' });
// Xử lý không đồng bộ
queue.add('process-webhook', req.body);
});
Cách Modern PetstoreAPI triển khai Webhook
Modern PetstoreAPI triển khai webhook sẵn sàng cho sản xuất.
Sự kiện Webhook
pet.adopted - Thú cưng đã được nhận nuôi
pet.status_changed - Trạng thái thú cưng đã thay đổi
order.created - Đơn hàng đã được tạo
order.completed - Đơn hàng đã hoàn thành
payment.succeeded - Thanh toán thành công
payment.failed - Thanh toán thất bại
Payload 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"
}
Cấu hình thử lại
- Số lần thử tối đa: 10
- Backoff: Lũy thừa (1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s)
- Tổng thời gian cửa sổ thử lại: ~17 phút
- Hàng đợi thư chết sau số lần thử lại tối đa
Bảo mật
- Chữ ký HMAC-SHA256 trong tiêu đề
X-Webhook-Signature - Xác thực dấu thời gian (từ chối webhook cũ hơn 5 phút)
- Yêu cầu HTTPS cho các URL webhook
Kiểm tra Webhook với Apidog
Apidog hỗ trợ kiểm tra webhook.
Kiểm tra việc gửi Webhook
- Tạo điểm cuối webhook giả lập trong Apidog
- Đăng ký điểm cuối với PetstoreAPI
- Kích hoạt sự kiện (nhận nuôi thú cưng)
- Xác minh webhook đã nhận
- Kiểm tra định dạng payload
Kiểm tra xác minh chữ ký
// 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('Chữ ký hợp lệ', () => {
pm.expect(signature).to.equal(`sha256=${expected}`);
});
Kiểm tra logic thử lại
- Trả về lỗi 500 từ điểm cuối giả lập
- Xác minh các lần thử lại với cơ chế backoff lũy thừa
- Kiểm tra hàng đợi thư chết sau số lần thử lại tối đa
Kiểm tra tính bất biến
- Nhận webhook
- Trả về 200
- Nhận lại cùng một webhook (thử lại mô phỏng)
- Xác minh không có xử lý trùng lặp
Kết luận
Webhook đáng tin cậy yêu cầu:
- Thử lại với cơ chế backoff lũy thừa (5-10 lần)
- Khóa bất biến để ngăn chặn trùng lặp
- Xác minh chữ ký HMAC để bảo mật
- Thời gian chờ 5 giây
- Xử lý không đồng bộ phía máy khách
- Hàng đợi thư chết cho các webhook thất bại
Modern PetstoreAPI triển khai tất cả các mẫu này. Kiểm tra tài liệu webhook để biết các ví dụ đầy đủ.
Kiểm tra webhook của bạn với Apidog để xác minh logic thử lại, chữ ký và tính bất biến trước khi đưa vào sản xuất.
Câu hỏi thường gặp
Webhook nên có bao nhiêu lần thử lại?
5-10 lần thử với cơ chế backoff lũy thừa. Điều này bao gồm các sự cố tạm thời (5-17 phút) mà không làm quá tải máy khách.
Webhook có nên thử lại khi có lỗi 4xx không?
Không. Lỗi 4xx cho thấy các vấn đề của máy khách (URL không đúng, lỗi xác thực). Việc thử lại sẽ không khắc phục được những lỗi này. Chỉ nên thử lại khi có lỗi 5xx và lỗi mạng.
Thời gian chờ của webhook nên là bao lâu?
Tối đa 5 giây. Máy khách nên trả về 200 ngay lập tức và xử lý không đồng bộ. Thời gian chờ dài hơn cho thấy máy khách đang thực hiện xử lý đồng bộ.
Điều gì xảy ra nếu máy khách không bao giờ phản hồi webhook?
Sau số lần thử lại tối đa, di chuyển đến hàng đợi thư chết. Thông báo cho khách hàng qua email. Cân nhắc tắt webhook cho khách hàng đó sau nhiều lần thất bại.
URL webhook có nên là HTTPS không?
Có, luôn yêu cầu HTTPS. Webhook HTTP có thể bị chặn và sửa đổi. Modern PetstoreAPI từ chối các URL webhook HTTP.
Làm cách nào để ngăn chặn các cuộc tấn công phát lại?
Bao gồm dấu thời gian trong payload và từ chối các webhook cũ hơn 5 phút. Kết hợp với xác minh chữ ký.
Khách hàng có thể yêu cầu gửi lại webhook không?
Có. Modern PetstoreAPI cung cấp một điểm cuối để gửi lại các webhook cụ thể: POST /webhooks/{id}/redeliver
Làm cách nào để kiểm tra webhook cục bộ?
Sử dụng các công cụ như ngrok để hiển thị localhost ra internet, hoặc sử dụng máy chủ giả lập của Apidog để mô phỏng các điểm cuối webhook trong quá trình phát triển.
