Tất cả bài viết
Kiểm thử Postmark webhook cục bộ qua HTTPS
Postmarkemail webhookslocalhostwebhook security

Kiểm thử Postmark webhook cục bộ qua HTTPS

Chạy handler cục bộ, mở cổng bằng npx portpreview PORT, rồi đăng ký route HTTPS với đúng Message Stream. Dùng Basic Authentication hoặc header bí mật, kiểm tra JSON, lưu idempotent và nhanh chóng trả HTTP 200.

Postmark gửi gì tới webhook

Postmark gửi HTTP POST cho các sự kiện Delivery, Bounce, Open, Click, Spam Complaint, Subscription Change và thư đến. Định tuyến theo RecordType. Delivery chỉ là máy chủ đích chấp nhận thư; Bounce có Type, TypeCode, Inactive, CanActivate. tổng quan webhook chính thức tài liệu bounce webhook

Tạo bộ nhận Express nhỏ trên máy

Ví dụ Express cổng 3000 kiểm tra Basic Auth, schema tối thiểu và lưu bền vững khóa chống trùng trước khi phản hồi. Thay helper bằng transaction hoặc queue; giới hạn kích thước và không log toàn bộ email nhạy cảm.

import crypto from 'node:crypto';
import express from 'express';

const app = express();
app.use('/webhooks/postmark', express.json({ limit: '2mb' }));

function safeEqual(actual, expected) {
  const a = Buffer.from(actual);
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

function authorized(req) {
  const value = req.get('authorization') ?? '';
  if (!value.startsWith('Basic ')) return false;
  const decoded = Buffer.from(value.slice(6), 'base64').toString('utf8');
  const separator = decoded.indexOf(':');
  if (separator < 0) return false;
  return safeEqual(decoded.slice(0, separator), process.env.POSTMARK_WEBHOOK_USER ?? '') &&
    safeEqual(decoded.slice(separator + 1), process.env.POSTMARK_WEBHOOK_PASSWORD ?? '');
}

app.post('/webhooks/postmark', async (req, res) => {
  if (!authorized(req)) return res.sendStatus(401);

  const event = req.body;
  if (typeof event?.RecordType !== 'string' ||
      typeof event?.MessageID !== 'string') {
    return res.status(400).json({ error: 'Invalid Postmark event' });
  }

  const deliveryKey = `${event.RecordType}:${event.MessageID}:${event.ID ?? ''}`;
  await saveWebhookOnce({ provider: 'postmark', deliveryKey, event });
  res.sendStatus(200);
});

app.listen(3000);

Công khai localhost bằng URL HTTPS

Kiểm tra cổng rồi chạy npx portpreview 3000, thêm route https://YOUR-TUNNEL.portpreview.dev/webhooks/postmark. Tunnel cung cấp HTTPS nhưng không xác thực Postmark. hướng dẫn bảo mật đường hầm localhost

Cấu hình đúng Postmark webhook

Chọn đúng Server và Message Stream, chỉ bật trigger đã hỗ trợ; inbound có URL riêng. API có HttpAuth, HttpHeaders; X-Postmark-Server-Token chỉ gọi API. Webhooks API

Xác thực Postmark không phải chữ ký mật mã

Postmark không hỗ trợ chữ ký HMAC webhook, không có signing secret hay X-Postmark-Signature. Basic Authentication, IP allowlist hoặc secret header không ràng buộc body bằng mật mã. Dùng HTTPS, validation, IP hiện hành và HttpAuth; không tái dùng Server API token.

Xử lý delivery và bounce theo loại

Đưa nghiệp vụ sang worker idempotent. Dùng phân loại Bounce hiện hành; Spam Complaint và Subscription Change là loại riêng, Open và Click có thể lặp.

async function processPostmarkEvent(event) {
  switch (event.RecordType) {
    case 'Delivery':
      await markAcceptedByRecipientServer({
        messageId: event.MessageID,
        deliveredAt: event.DeliveredAt
      });
      break;
    case 'Bounce':
      await recordBounce({
        bounceId: String(event.ID),
        messageId: event.MessageID,
        type: event.Type,
        inactive: event.Inactive,
        canActivate: event.CanActivate
      });
      break;
    default:
      await recordUnhandledPostmarkType(event.RecordType);
  }
}

Thiết kế cho retry và bản giao trùng

Không có HTTP 200 thì Postmark retry; 403 dừng retry. Dùng unique key từ MessageID, thêm RecordTypeID khi cần. Trả 200 sau durable handoff và gọi API ngoài bất đồng bộ. retry và idempotency cho webhook

Kiểm thử sự kiện thật an toàn

Gửi curl POST trước, rồi thử Delivery tới địa chỉ của bạn và Bounce bằng công cụ Postmark như black-hole test domain nếu hỗ trợ. Lưu MessageID và gửi fixture sạch hai lần để kiểm tra idempotency.

Khắc phục lỗi Postmark webhook thường gặp

Request không tới endpoint

Kiểm tra tunnel, route, port. Với 401, kiểm tra credential và proxy không xóa Authorization. Với retry, kiểm tra status 200 và latency. So khớp RecordType, trigger và stream nếu payload khác.

Mọi request đều trả HTTP 401

hướng dẫn lỗi webhook 401/403

Vẫn retry sau khi xử lý

Kiểm tra tunnel, route, port. Với 401, kiểm tra credential và proxy không xóa Authorization. Với retry, kiểm tra status 200 và latency. So khớp RecordType, trigger và stream nếu payload khác.

Payload không khớp mẫu

Kiểm tra tunnel, route, port. Với 401, kiểm tra credential và proxy không xóa Authorization. Với retry, kiểm tra status 200 và latency. So khớp RecordType, trigger và stream nếu payload khác.

Danh sách bảo mật production

Dùng HTTPS và credential riêng mạnh; tách API token; rotate sau test; xóa URL cũ; validate loại, kích thước và trường; che dữ liệu trong log; dùng least privilege; theo dõi lỗi, lag, duplicate và dead letter.

hướng dẫn gỡ lỗi webhook cục bộ

Câu hỏi thường gặp

Cách kiểm thử Postmark webhook trên localhost?
Chạy handler cục bộ, mở cổng bằng <code>npx portpreview PORT</code>, rồi đăng ký route HTTPS với đúng Message Stream. Dùng Basic Authentication hoặc header bí mật, kiểm tra JSON, lưu idempotent và nhanh chóng trả HTTP 200.
Postmark có ký webhook bằng HMAC không?
Không. Hãy dùng HTTPS, Basic Authentication và kiểm tra từng payload.
Vì sao webhook được gửi lại?
Khi thiếu HTTP 200 sẽ có retry; hãy lưu khóa sự kiện duy nhất.
Delivery có nghĩa là email đã được đọc?
Không. Chỉ có nghĩa máy chủ đích đã chấp nhận thư.