コンテンツへスキップ

Webhookの基礎 — ポーリングとの違いと、受け取る側が考えておくこと

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では、相手の都合で、いつでも呼ばれる。これは、次のような前提を受け入れることを意味する。

  1. 常に受け付けられる状態でいる必要がある: 受け取る側が停止中だと、通知を取りこぼす可能性がある
  2. 誰からでも呼べるURLになる: エンドポイントはインターネットに公開されるので、本物の送信元かどうかを確かめる仕組みが要る
  3. 同じ通知が複数回届くことがある: 後述のとおり、多くの送信側は再送する
  4. 順番どおりに届くとは限らない: 「作成」より先に「更新」が届く場面もありうる

以降、この4つを順に見ていく。

1. 受け取ったら、まず返事をする

Webhookの送信側は、送ったリクエストに対して、一定時間内に成功のステータスコードが返ってくることを期待していることが多い。返事が遅い、またはエラーが返ると、「届かなかった」と判断して再送する設計が一般的である。

そのため、受け取る側は次の順で作ると扱いやすい。

  1. 受け取った内容を、まず保存する(キューに積む)
  2. すぐに成功のステータスコードを返す
  3. 重い処理は、あとから別に実行する

受け取った場で時間のかかる処理まで済ませようとすると、タイムアウトによる再送を招き、同じ通知が重なって届く原因になる。

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を遮断していないかを確認する。通知が届かない原因が、受け取る側の前段にあることは多い
  • 公開エンドポイントは攻撃の入口にもなりうるので、更新を怠らず、不要になった連携は止める

自分の連携を点検する観点

  1. 通知を受けたら、重い処理の前に成功を返せているか
  2. 送信元の検証を、処理の最初に、公式の方式で行っているか
  3. 同じ通知が2回届いても、結果が変わらないか
  4. 順番が前後しても、状態が壊れないか
  5. 取りこぼしたときに、気付く方法と取り戻す方法があるか
  6. 受け取った内容を記録しているか

まとめ

  • Webhookは、相手側の出来事を、相手から知らせてもらう仕組み。ポーリングの「空振り」と「遅れ」のトレードオフを避けられる
  • 代わりに、受け取る側は外から呼ばれるURLを公開する立場になる
  • 設計の要点は、先に返事をする・送信元を検証する・重複に備える・順番の前後に備える・取りこぼしに備えるの5つ
  • 署名による検証は、サービスの公式の方式に従う。独自の判定は作らない
  • Webhookは「合図」と割り切り、必要なら相手に確認して取り直すと堅牢になる

「呼ばれる側になる」ことの意味を、作り始める前に整理しておくと、あとからの手戻りが少なくなる。