Thiết Kế Webhooks Tin Cậy Như Thế Nào?

Ashley Innocent

Ashley Innocent

13 tháng 3 2026

Thiết Kế Webhooks Tin Cậy Như Thế Nào?

Apidog cho doanh nghiệp

Triển khai tại chỗ

SSO & RBAC

Tuân thủ SOC 2

Khám phá Apidog Enterprise

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.

💡
Nếu bạn đang xây dựng hoặc thử nghiệm webhook, Apidog giúp bạn kiểm tra việc gửi webhook, xác thực chữ ký và mô phỏng các tình huống lỗi. Bạn có thể kiểm tra logic thử lại và xác minh cách xử lý tính bất biến.
nút

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:

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:

Không thử lại khi:

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

Bảo mật

Kiểm tra Webhook với Apidog

Apidog hỗ trợ kiểm tra webhook.

Kiểm tra việc gửi Webhook

  1. Tạo điểm cuối webhook giả lập trong Apidog
  2. Đăng ký điểm cuối với PetstoreAPI
  3. Kích hoạt sự kiện (nhận nuôi thú cưng)
  4. Xác minh webhook đã nhận
  5. 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

  1. Trả về lỗi 500 từ điểm cuối giả lập
  2. Xác minh các lần thử lại với cơ chế backoff lũy thừa
  3. 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

  1. Nhận webhook
  2. Trả về 200
  3. Nhận lại cùng một webhook (thử lại mô phỏng)
  4. 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:

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.

nú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.

Thực hành thiết kế API trong Apidog

Khám phá cách dễ dàng hơn để xây dựng và sử dụng API