Một webhook trả về 401 hoặc 403 rơi vào một số ít nhóm. Hầu hết thời gian nó thậm chí không phải vấn đề chữ ký — mà là một middleware hoặc giá trị mặc định của framework từ chối yêu cầu trước khi mã của bạn chạy. Đây là checklist chẩn đoán chúng tôi dùng, theo thứ tự chúng tôi áp dụng.
Trước hết, tách 401 khỏi 403
Chúng nghĩa khác nhau và cách sửa khác nhau.
401 Unauthorized: máy chủ nhận yêu cầu, xem thông tin xác thực (hoặc chữ ký) và không chấp nhận. Handler của bạn có lẽ đã chạy, tính một chữ ký mong đợi và từ chối.
403 Forbidden: máy chủ nhận yêu cầu và từ chối xử lý vì lý do khác. Thường yêu cầu chưa bao giờ tới handler của bạn — một middleware hoặc mặc định framework đã gửi 403.
Mở phần bắt yêu cầu của tunnel và xem phản hồi. Nếu bạn thấy body phản hồi của handler ("invalid signature"), đó là 401 từ mã của bạn. Nếu phản hồi chung chung và log handler không hiện gì, framework đã từ chối trước khi handler chạy.
Năm nguyên nhân khả dĩ nhất
1. Middleware CSRF (Django, Rails, Laravel)
Bảo vệ CSRF mặc định từ chối POST không có session token. Nhà cung cấp webhook không gửi. Triệu chứng: 403, body phản hồi chung chung, không có log handler.
Sửa: loại route webhook khỏi bảo vệ CSRF. Trong Django dùng @csrf_exempt. Trong Rails dùng skip_before_action :verify_authenticity_token, only: [:webhook]. Trong Laravel thêm path vào VerifyCsrfToken::$except. Hướng dẫn webhook Django trình bày phiên bản Django từ đầu đến cuối.
2. Middleware auth áp dụng quá rộng
Bạn thêm middleware auth toàn cục (JWT, kiểm tra session, yêu cầu API key) và route webhook thừa hưởng nó. Nhà cung cấp không gửi header auth của bạn, nên middleware gửi 401 trước khi handler chạy.
Sửa: loại path webhook khỏi middleware auth. Xác thực webhook là chữ ký, không phải sơ đồ bảo vệ phần còn lại của API.
3. Xác minh chữ ký thất bại
Handler đã chạy, tính chữ ký mong đợi và không khớp. Năm nguyên nhân con, đại khái theo thứ tự tần suất giảm dần:
- Body bị middleware phân tích trước khi xác minh chạy (body thô đã mất).
- Sai mã hóa (hex vs base64). Xem hướng dẫn xác minh chữ ký.
- Sai secret (test vs live, dashboard vs CLI, file env vs runtime).
- Timestamp quá cũ (chữ ký hợp lệ nhưng cũ — có lẽ bạn đang kiểm thử bằng payload cũ phát lại).
- Khoảng trắng cuối trong secret nạp từ file env.
4. Đăng ký sai URL tunnel
Bạn khởi động lại tunnel và URL xoay vòng, nhưng dashboard nhà cung cấp vẫn còn cái cũ. Triệu chứng trông như 401 vì yêu cầu chưa bao giờ tới bạn — nhưng thực ra một yêu cầu khác tới một máy chủ khác (thường là phiên tunnel trước, giờ từ chối hoặc trả về 401).
Sửa: xác nhận URL trong dashboard nhà cung cấp khớp với phiên tunnel hiện tại. Nếu cần URL ổn định, hãy xem tunnel có tên hoặc subdomain dành riêng.
5. Hạn chế CORS, content-type hoặc method
Ít gặp với webhook nhưng có thể. Nếu route của bạn chỉ chấp nhận application/json mà nhà cung cấp gửi application/x-www-form-urlencoded (ví dụ Twilio), một số framework trả 415 — nhưng cấu hình sai có thể trả 403. Hoặc route đăng ký cho GET mà nhà cung cấp POST.
Quy trình chẩn đoán 90 giây
Đây là thứ tự chúng tôi đi qua:
- Đọc body phản hồi. Nếu có chữ của handler, yêu cầu đã tới handler. Chuyển sang gỡ lỗi chữ ký. Nếu là trang lỗi framework chung chung, yêu cầu chưa tới. Chuyển sang gỡ lỗi middleware.
- Kiểm tra log handler. Có câu lệnh log nào của handler kích hoạt không? Xác nhận yêu cầu có tới bạn không.
- Kiểm tra URL đã đăng ký. Mở dashboard nhà cung cấp. Xác nhận URL khớp tunnel hiện tại và trỏ đúng path.
- So sánh header đến. Bắt tunnel cho bạn thấy chính xác header nhà cung cấp gửi. So với cái mã của bạn đọc. Header không phân biệt hoa thường nhưng cách truy cập khác nhau theo framework —
request.headers.get('Stripe-Signature')ở vài cái,request.META['HTTP_STRIPE_SIGNATURE']ở cái khác. - Xác minh body là thô tại thời điểm ký. In độ dài body ngay trước khi tính chữ ký. Nếu bằng không hoặc nhỏ bất thường, một middleware đã ăn nó.
- Kiểm tra lại secret. So biến env trong runtime với dashboard.
console.log(process.env.WEBHOOK_SECRET.length)— độ dài có khớp cái dashboard hiện không?
Theo kinh nghiệm của chúng tôi, bước 1 và 5 bắt 80% trường hợp trong hai phút đầu.
Bắt yêu cầu thất bại
Công cụ hữu ích nhất ở đây là một tunnel với bắt và phát lại yêu cầu. Bạn không cần chờ nhà cung cấp thử lại — bạn phát lại yêu cầu đã bắt vào handler cục bộ trong khi gỡ lỗi. Mỗi lần thử là tức thì.
Nếu bạn dùng một tunnel không bắt yêu cầu, bạn gỡ lỗi với một tay bị trói sau lưng. Chuyển sang cái có (hoặc chạy tcpdump, hoặc đặt nginx trước máy chủ dev) trả công ngay lần đầu bạn tiết kiệm được một giờ.
401 trông thế nào từ mỗi nhà cung cấp
- Stripe: 401 từ mã của bạn nghĩa là xác minh chữ ký thất bại. Dashboard Stripe hiển thị lần gửi là thất bại và bao gồm body phản hồi của bạn.
- GitHub: nếu handler trả về 401, GitHub đánh dấu lần gửi là thất bại và thử lại. Trang các lần gửi gần đây hiển thị phản hồi.
- Shopify: 401 từ một handler webhook là ổn về bảo mật nhưng Shopify sẽ thử lại. Sau 19 lần thất bại liên tiếp trong 48 giờ, Shopify vô hiệu hóa đăng ký.
Để có bối cảnh gỡ lỗi webhook rộng hơn, xem cách gỡ lỗi webhook cục bộ. Tham gia danh sách chờ của PortPreview để có một tunnel với bắt yêu cầu tích hợp sẵn.