顧客からメールが届きます:「Stripe の Webhook が壊れている」。Stripe のダッシュボードを確認すると、イベントは発火しています。サーバーのログを確認しても、何もありません。Sentry にも何もありません。イベントはどこかに向かっています。ただ、期待している場所ではないだけです。
この記事は、よく起きる障害のパターンをもとにした、本番環境で Webhook の配信の問題をデバッグするための段階的な手順です。
TL;DR
- まずはプロバイダーのダッシュボードから:イベントは発火しているか、どのステータスが返っているか?
4xxは、こちらが拒否しているということです(署名、ミドルウェア、URL の間違い)5xxは、ハンドラーがクラッシュしたということです。そのタイムスタンプちょうどのエラー監視を確認します- タイムアウトは、ハンドラーが遅すぎるということです。重い処理は非同期に移し、200 をすばやく返します
- 「デバッグに 2 時間かかった」を解決するのは、受信、送信、リトライ、再送を 1 か所でまとめて見られることです。たいていは Webhook 中継サービスを意味します
判断ツリー
ここから始めてください。30 分の当て推量を省けます。
Q1:プロバイダーは本当にイベントを送っているか?
プロバイダーのダッシュボードを確認します。Stripe なら Developers → Events(開発者 → イベント)、GitHub なら Settings → Webhooks → Recent Deliveries(最近の配信)、Shopify なら Dev Dashboard → Apps → 自分のアプリ → Logs です。
イベントがなければ、トリガーが発火していません。イベントを生成するはずの上流の状態をデバッグしてください。イベントがあれば、Q2 に進みます。
Q2:プロバイダーは 2xx を受け取っているか?
同じダッシュボードで、エンドポイントからのレスポンスのステータスを見ます。
2xx:プロバイダーは成功したと考えています。アプリがそのイベントを一度も見ていないなら、イベントは届いたものの、レスポンスの後に失われています。非同期キュー、DB への書き込み、ハンドラーのロジックを確認してください。4xx:こちらが拒否しています。いちばん多いのは、署名検証の失敗、ヘッダーの欠落、またはミドルウェア(WAF、レートリミッター、認証レイヤー)によるリクエストのブロックです。5xx:エンドポイントがクラッシュしています。エラー監視を確認してください。「直近 1 時間」ではなく、この配信のタイムスタンプちょうどを見ます。- タイムアウト:エンドポイントが遅すぎます。Q4 を参照してください。
- 接続拒否や DNS のエラー:エンドポイントに到達できません。これは別の問題で、ネットワーク、DNS、ファイアウォールのどこかにあります。
Q3:4xx なら、どれか?
400:不正なリクエストです。たいていはヘッダーの不一致か、署名チェックが明確なエラーを返す前に例外を投げています。ボディの形式が崩れているときや署名がないときに、ハンドラーが何を返すかを確認してください。401/403:リクエストを認可されていないものとして拒否しています。署名検証が失敗していませんか?セッションが不要なはずのエンドポイントで、ミドルウェアがユーザーセッションを確認していませんか?404:URL が間違っています。Webhook のエンドポイントを移動するようなルート変更をデプロイしていませんか?413:ペイロードが大きすぎます。body-parser の上限を引き上げてください。429:レートリミッターが作動しています。Webhook のルートにレート制限をかけているものは、何であれ外すべきです。ほとんどの Webhook プロバイダーは限られた IP から送ってくるので、リミッターがそれらをまとめて 1 人の攻撃者として扱っているのかもしれません。
Q4:タイムアウトなら、何が遅いのか?
GitHub の猶予は 10 秒です。Shopify は 5 秒、Slack と Discord は 3 秒です。Stripe は数字を公開しておらず、ドキュメントには、時間のかかる処理の前に 2xx を返すようにとしか書かれていません。行ごとに出典を付けた完全な表は、Webhook プロバイダー別リファレンスにあります。
タイムアウトのほとんどは、ハンドラーの中で処理をしすぎることから起きます。よくある原因は、飽和したコネクションプールへの同期的なデータベース書き込み、ハンドラーの中からの別サービス(Stripe、Salesforce、Notion、自前の API)の呼び出し、メールや Slack メッセージの同期的な送信、そしてバックプレッシャーがかかっているログ収集基盤に向けた、ボディ全体の stdout への出力です。
直し方のパターン:署名を検証し、イベントをキューに入れ、200 を返します。ビジネスロジックはすべて、下流で非同期に行います。
実際によく壊れているところ
何年かこれを見てきた結果、80/20 で言うとこうなります:
- 署名検証の前にボディを再シリアライズしています。署名検証ガイドを参照してください
- プロキシや WAF が、ハンドラーに届く前に Webhook を握りつぶしています。Cloudflare、Vercel など、使っているプラットフォームのリクエストログを確認してください
- レート制限が作動しています。Cloudflare の Bot Management は、正当な Webhook を誤検知することがあります
- デプロイでエンドポイントの URL が変わったのに、誰もプロバイダー側を更新していません
- 本番環境で環境変数(署名シークレット、DB の URL)が欠けていて、ハンドラーが黙って 500 を返しています
- Webhook が 200 を返した後で、バックグラウンドジョブがクラッシュしています。プロバイダーは問題ないと思っていますが、何も処理されていません
6 番目はサーバーレスでは日常茶飯事です。レスポンスの後に予定した処理には、永続性の保証がまったくないからです:「Vercel で Webhook を取りこぼさずに受信する」を参照してください。症状が取りこぼしではなく重複なら、それはたいてい設計どおりの動作です:同じ Webhook を 2 回受け取ったときの記事を参照してください。
本当に役立つツール
プロバイダーのダッシュボードは、イベントを 1 件ずつ見るには便利ですが、パターンを見るには向きません。「直近 24 時間に失敗したすべてのイベント」を見たければ、手作業でスクロールすることになります。
ngrok と webhook.site はローカル開発用です。本番環境では役に立ちません。
自分のログは、正しいものを記録していれば役に立ちます。署名チェックの前に「webhook received for event evt_xxx」と記録していれば、少なくとも届いたことはわかります。ほとんどの人は後に記録するので、署名検証に失敗したイベントは見えなくなります。
AnyHook のような Webhook 中継サービスはハンドラーの前に置かれ、ヘッダー、ボディ、所要時間、レスポンスのステータス、試行回数を記録します。デバッグの流れは、イベントの一覧を見て、失敗しているものをクリックし、生のペイロードを確認して再送する、というものになります。
AnyHook でのデバッグの流れ
ユーザーから AnyHook のサポートに「Webhook が動かない」というメールが来たとき、ダッシュボードを見れば、たいてい 1 分もかからずに原因がはっきりします:
- イベントの一覧を開き、
status=failedで絞り込みます - 失敗しているイベントをクリックします
- 受信したヘッダー(署名はあったか?)、受信したボディ(期待した形だったか?)、転送先からのレスポンス(サーバーは何と返したか?)、所要時間の内訳(エッジ → キュー → 配信)を確認します
- まずいデプロイや不安定なネットワークによる一度きりの失敗なら、Replay(再送)をクリックして、成功するのを確認します
- すべてのイベントが失敗しているなら、何を直すべきかはもう正確にわかっています。直している間も、イベントは保存されたままで、プランの再送可能期間(Free は 3 日、Pro は 30 日、Scale は 90 日)のあいだは再送できます
2 時間のデバッグを 2 分に変えるのは、プロバイダーが実際に送ったリクエスト、サーバーが実際に返したレスポンス、その間のタイムスタンプが、すべて 1 か所にそろっていることです。それがなければ、3 つのダッシュボードと自分のログから組み立て直すことになります。
何かに登録する前に試すなら、アカウントなしで、コマンド 1 つで実際に使える受信 URL が返ってきます:
curl -X POST https://anyhook.net/api/v1/quickstart
レスポンスには、プロバイダーに設定する inbound_url、API 経由でイベントログを読むための api_key、そして claim_url が含まれます。このエンドポイントは一時的なものです:紐付け用の URL を開いてログインし、アカウントに紐付けない限り、エンドポイントとそのイベントは 7 日後に削除されます。紐付ければ同じ URL をそのまま使えます。
まとめ
日曜の午後がこれでつぶれることが思っている以上に多いなら、AnyHook を使えば、そのほとんどがダッシュボードでのクリックで済みます。