Webhookの基礎 — ポーリングとの違いと、受け取る側が考えておくこと
「注文が入ったら通知したい」「決済が完了したら状態を更新したい」。外部サービスと連携していると、相手側で起きた出来事を、こちらがどう知るかという問題に必ず行き当たる。
その方法は大きく2つある。こちらから何度も聞きに行くポーリングと、相手から知らせてもらうWebhookである。この記事では、両者の違いを小さな計算で確かめ、Webhookを受け取る側が設計段階で考えておくことを整理する。
補足: 数値は、考え方を確かめるための机上の計算と、Python 標準ライブラリでの最小の実験によるもの。特定のサービスの実測ではない。なお、送信元の検証については、具体的な手順ではなく、設計の考え方に絞って書く。
Webhookとは何か
Webhookは、相手側で出来事が起きたとき、あらかじめ登録しておいたURLへ、相手のほうからHTTPリクエストを送ってくる仕組みである。「逆向きのAPI」と呼ばれることもある。
- 通常のAPI: こちらが相手に問い合わせ、相手が答える
- Webhook: 相手が出来事を知らせ、こちらが受け取る
受け取る側は、外部から呼ばれるURL(エンドポイント)を1つ用意しておくだけでよい。
ポーリングとの違いを計算で見る
ポーリングは「変わったかどうか」を一定間隔で問い合わせ続ける方法である。1日に出来事が3件だけ起きる場合を考える。
| 方式 | 1日のリクエスト数 | 出来事を知るまでの遅れ |
|---|---|---|
| ポーリング(60秒おき) | 1,440回(うち空振り1,437回) | 平均30秒・最大60秒 |
| ポーリング(5分おき) | 288回(うち空振り285回) | 平均150秒・最大300秒 |
| Webhook | 3回 | 出来事の直後 |
ポーリングは、間隔を短くすると遅れは減るが、空振りが増える。間隔を長くすると空振りは減るが、遅れが増える。この2つはトレードオフの関係にある。
Webhookは、出来事が起きたときだけ通信が発生するので、この両方を避けられる。ただし、その代わりに、受け取る側が「外から呼ばれる」立場になることで、別の設計課題が生まれる。
受け取る側が負う前提
ポーリングでは、こちらが問い合わせるタイミングを決められる。Webhookでは、相手の都合で、いつでも呼ばれる。これは、次のような前提を受け入れることを意味する。
- 常に受け付けられる状態でいる必要がある: 受け取る側が停止中だと、通知を取りこぼす可能性がある
- 誰からでも呼べるURLになる: エンドポイントはインターネットに公開されるので、本物の送信元かどうかを確かめる仕組みが要る
- 同じ通知が複数回届くことがある: 後述のとおり、多くの送信側は再送する
- 順番どおりに届くとは限らない: 「作成」より先に「更新」が届く場面もありうる
以降、この4つを順に見ていく。
1. 受け取ったら、まず返事をする
Webhookの送信側は、送ったリクエストに対して、一定時間内に成功のステータスコードが返ってくることを期待していることが多い。返事が遅い、またはエラーが返ると、「届かなかった」と判断して再送する設計が一般的である。
そのため、受け取る側は次の順で作ると扱いやすい。
- 受け取った内容を、まず保存する(キューに積む)
- すぐに成功のステータスコードを返す
- 重い処理は、あとから別に実行する
受け取った場で時間のかかる処理まで済ませようとすると、タイムアウトによる再送を招き、同じ通知が重なって届く原因になる。
2. 本物の送信元かどうかを確かめる
エンドポイントのURLは公開されている。そのため、送信元になりすました偽の通知が届く可能性を、最初から設計に織り込んでおく必要がある。
多くのサービスは、この問題に対して、送信側と受信側があらかじめ秘密の値を共有し、その値から計算した「署名」をリクエストに添える方式を用意している。受け取る側は、同じ計算をして、一致するかを確かめる。
仕組みの感覚をつかむために、Python 標準ライブラリで、同じ本文に対する署名を計算してみた。
| 比較 | 結果 |
|---|---|
| 同じ本文・同じ秘密の値で2回計算 | 署名が一致する |
| 本文に1バイト足して計算 | 署名がまったく別の値になり、一致しない |
つまり、本文が1バイトでも変わっていれば、署名の照合で気付ける。秘密の値を知らない第三者は、正しい署名を作れない。この性質が、「通知が本物であること」と「途中で書き換えられていないこと」の根拠になる。
ここで押さえておきたいのは、具体的な手順よりも、次の考え方である。
- 署名の照合は、処理の最初に行う。照合に通る前の内容は、何も信用しない
- 署名の仕様は、サービスごとに異なる。必ず、そのサービスの公式ドキュメントに従う。自分で独自の判定を作らない
- 秘密の値は、ソースコードや公開される場所に置かない。漏れた場合に差し替えられる運用にしておく
- 古い通知を使い回す攻撃(リプレイ)への備えとして、サービスが送信時刻を含めている場合は、その鮮度も確認する
この記事では、検証の具体的なコードや判定の細部には踏み込まない。実装するときは、利用するサービスの公式ドキュメントと、信頼できる公式ライブラリを出発点にするのがよい。
3. 同じ通知が2回届いても安全にする
送信側は、「届いたかどうか確信できない」ときに再送する。受け取る側が成功を返せなかった場合はもちろん、返したのに通信の都合で送信側に届かなかった場合でも、再送は起きる。
つまり、Webhookは多くの場合「少なくとも1回は届く」ことを保証する設計で、「ちょうど1回」は保証しない。同じ通知が重複して届く前提で、受け取る側が備える必要がある。
ここで効いてくるのが、冪等性の考え方である。詳しくは 冪等性(idempotency)とは で整理したが、Webhookでは次のように使う。
- 通知に付いている一意のIDを記録しておき、処理済みのIDなら何もせず、成功だけを返す
- 「在庫を1つ減らす」のような差分ではなく、「在庫を〇個にする」のように状態を指定する形で更新できないか考える
- ID の記録と業務処理は、同じトランザクションにまとめる。別々にすると、片方だけ成功した状態が残る
4. 順番が前後しても壊れないようにする
通知は、再送や経路の違いによって、出来事が起きた順に届くとは限らない。たとえば、「注文が確定した」より先に「注文が発送された」が届くことがある。
対策として、次のような考え方がある。
- 通知に時刻や連番が含まれているなら、すでに反映済みの状態より古い通知は無視する
- 通知の内容だけを信じず、必要なら相手のAPIへ問い合わせて最新の状態を取り直す。通知を「何かが起きたという合図」と位置付け、真実は相手に確認する
後者は、ポーリングとWebhookを組み合わせる考え方でもある。Webhookで素早く気付き、取りこぼしに備えて、低頻度のポーリングで突き合わせる。
取りこぼしに備える
どれだけ作り込んでも、受け取る側の停止やネットワークの断絶で、通知を取りこぼす可能性はゼロにならない。多くの送信側は、一定回数・一定期間で再送をやめる。
そこで、次の備えを持っておくと安心である。
- 受信したリクエストを記録する(本文、受信時刻、検証結果、処理結果)。あとから「届いていたのか」を調べられる
- 送信側に再送機能や、通知の履歴を見る画面があるか確認しておく
- 取りこぼしを前提に、定期的な突き合わせ(相手のAPIとの照合)を用意する
WordPressの視点から
WordPressサイトでWebhookを受け取る場合、プラグインやテーマが用意するエンドポイントに、外部サービスから通知が届く構成が一般的である。
- 受け取る処理が重いと、管理画面の操作と同じサーバー資源を使うので、受け取りは軽く、本処理は別に走らせる
- キャッシュやセキュリティ関連のプラグインが、外部からのPOSTを遮断していないかを確認する。通知が届かない原因が、受け取る側の前段にあることは多い
- 公開エンドポイントは攻撃の入口にもなりうるので、更新を怠らず、不要になった連携は止める
自分の連携を点検する観点
- 通知を受けたら、重い処理の前に成功を返せているか
- 送信元の検証を、処理の最初に、公式の方式で行っているか
- 同じ通知が2回届いても、結果が変わらないか
- 順番が前後しても、状態が壊れないか
- 取りこぼしたときに、気付く方法と取り戻す方法があるか
- 受け取った内容を記録しているか
まとめ
- Webhookは、相手側の出来事を、相手から知らせてもらう仕組み。ポーリングの「空振り」と「遅れ」のトレードオフを避けられる
- 代わりに、受け取る側は外から呼ばれるURLを公開する立場になる
- 設計の要点は、先に返事をする・送信元を検証する・重複に備える・順番の前後に備える・取りこぼしに備えるの5つ
- 署名による検証は、サービスの公式の方式に従う。独自の判定は作らない
- Webhookは「合図」と割り切り、必要なら相手に確認して取り直すと堅牢になる
「呼ばれる側になる」ことの意味を、作り始める前に整理しておくと、あとからの手戻りが少なくなる。