Hầu hết bug webhook Shopify trong phát triển cục bộ quy về một dòng: HMAC được mã hóa base64, không phải hex. Nếu bạn gọi .digest('hex') và so với X-Shopify-Hmac-Sha256, kiểm tra sẽ không bao giờ qua — kể cả với shared secret đúng. Chúng tôi đã thấy kỹ sư senior mất cả buổi chiều vì một ký tự đó.
Cái bẫy base64
Stripe dùng hex. GitHub dùng hex (với tiền tố sha256=). Shopify dùng base64. Ba nhà cung cấp, ba kiểu mã hóa. Phiên bản đầu của trình xác minh gần như chắc chắn sẽ copy một snippet từ dự án khác và thất bại âm thầm.
Đây là cách xác minh nên trông như sau:
const hmac = crypto
.createHmac('sha256', SHOPIFY_API_SECRET)
.update(rawBody) // raw body, not JSON-parsed
.digest('base64'); // base64, not hex
const valid = crypto.timingSafeEqual(
Buffer.from(hmac),
Buffer.from(req.headers['x-shopify-hmac-sha256']),
);
Ngoài mã hóa, hai điều quan trọng. Body phải là byte thô, chưa parse. Và so sánh phải timing-safe — so bằng chuỗi rò rỉ thông tin từng byte một.
Vì sao một tunnel làm việc này dễ hơn
Bạn cũng có thể dùng lệnh Shopify shopify app dev, vốn dựng một tunnel nội bộ. Nó chạy được. Nhưng nó cũng bọc máy chủ dev của bạn trong tiến trình riêng, nuốt log theo một cách nhất định, và không cho bạn nút replay. Cho phát triển app vượt quá "hello world", một URL công khai ổn định cộng với tunnel localhost có bắt yêu cầu tiết kiệm nhiều thời gian hơn CLI tiết kiệm ở thiết lập ban đầu.
npx portpreview 3000
Bạn dán URL HTTPS vào cấu hình webhook của app trong dashboard Partners, kích hoạt một sự kiện từ một test store, và yêu cầu đáp xuống handler cục bộ với tất cả header còn nguyên.
Test store: phần tài liệu Shopify lướt qua
Hai điều cần biết trước khi nối bất cứ thứ gì:
- Store development bắn tất cả sự kiện webhook. Tạo đơn, fulfillment, tồn kho — tất cả. Bạn không cần giả lập gì; chỉ cần cài app lên store dev và click loanh quanh.
- Secret webhook khác nhau theo app và theo kênh giao. Nếu bạn cũng subscribe EventBridge hay Pub/Sub, hành vi HMAC khác. Ở đây ta nói về giao webhook HTTPS thuần.
Thiết lập từng bước
- Khởi động app cục bộ trên cổng bạn dùng (3000 phổ biến).
- Chạy
npx portpreview 3000và sao chép URL HTTPS nó in ra. - Trong dashboard Shopify Partners, mở app và vào Configuration → Webhooks.
- Đặt endpoint webhook thành
https://your-tunnel.portpreview.dev/api/webhooks/shopify(hoặc path app bạn dùng). - Cài app lên store dev, rồi kích hoạt một sự kiện — tạo đơn nháp, fulfill một mục, đổi tồn kho.
- Xem yêu cầu đáp xuống. Kiểm tra header và body. Replay yêu cầu đã bắt sau mỗi lần sửa handler.
Những lỗi chúng tôi cứ thấy
Body bị parse trước khi xác minh
Nếu bạn đặt app.use(express.json()) trước route webhook, byte thô đã mất khi bạn cố xác minh. Mount một parser body thô chỉ trên path webhook, hoặc kéo body thủ công từ request stream trước bất kỳ parse JSON nào.
Nhầm lẫn secret
Secret app Partners không giống token Storefront API. HMAC webhook dùng secret app. Nếu bạn đang nhìn shp_xxx trong file env, bạn lấy nhầm rồi.
Sự kiện test trông giống hệt nhau
Nút "Send test notification" của Shopify giao một sự kiện tổng hợp với body giữ chỗ. Chữ ký trên sự kiện test đó là thật, nhưng payload cố định. Để test hình dạng payload thực tế, kích hoạt sự kiện từ chính store dev.
Replay không thể thương lượng cho dev app
Shopify thử lại webhook thất bại tới 48 giờ với backoff lũy thừa. Hào phóng, nhưng khi lặp bạn không muốn chờ lần thử kế. Bắt lần giao đầu tiên trong lịch sử yêu cầu của tunnel và replay theo nhu cầu vào handler cục bộ.
Khi cục bộ không còn đủ
Luồng đăng ký shop, webhook GDPR và thay đổi billing subscription dễ xác thực hơn trên môi trường đã deploy vì chúng tương tác với trạng thái tài khoản của chính Shopify. Còn mọi thứ khác — parse payload, xác minh chữ ký, logic nghiệp vụ — cục bộ nhanh hơn.
Về toán chữ ký nền tảng trên mọi nhà cung cấp, đọc hướng dẫn xác minh chữ ký webhook của chúng tôi. Tham gia danh sách chờ PortPreview nếu bạn muốn một tunnel tích hợp sẵn replay.