Go Webhook Receiver Starter

Webhook を安全に受け取る小さな Go サービスです。署名検証、重複排除、永続化、リトライまで済んでいて、自分で書くのは一つの関数だけです。

Polar で購入: $24 買い切り · v0.2.1 · Polar からすぐにダウンロード

Go Webhook Receiver Starter のカバー画像
モックアップではなく、実際のテスト実行の出力です。

内容

フレームワークではなく、午後ひと休みのあいだに読み切れる完成品のサービスです。自分で書くロジックは handle() の一つの関数だけ。周りの処理はすべて実装とテストが済んでいます。

sender --POST--> /webhook/{source}
                   1. read the body (size limit)
                   2. verify the HMAC signature   -> 401 if wrong, nothing stored
                   3. dedupe by delivery id       -> the same event twice is handled once
                   4. insert into SQLite          -> 202 Accepted, fast
worker ----------> handle(event)   retries with backoff, dead after N attempts

機能

  • 三種類の署名方式。 GitHub 形式、Stripe 形式(リプレイ対策の許容時間つき)、Polar が採用している Standard Webhooks に対応します。比較は定数時間で行います。
  • シークレットのローテーション。 WEBHOOK_SECRET にカンマ区切りで複数指定できるので、ローテーション中は新旧どちらも受け付け、イベントを取りこぼしません。
  • 重複排除。 配信 ID で行います(Standard Webhooks では webhook-id ヘッダーを自動で使います)。ID がない場合はボディのハッシュで判定します。一意制約で保証しているので、再起動をまたいでも有効です。
  • 永続キュー。 SQLite の WAL モードを使い、リクエストは一回のコミットのあとに受理を返します。指数バックオフ、MAX_ATTEMPTS 回を超えたらデッドレターへ、クラッシュで processing のまま残ったイベントの復旧も備えます。
  • 並行処理。 WORKERS=N でワーカーを増やせます。同じイベントが二つのワーカーに渡ることはありません。
  • 運用機能。 デッドになったイベントを再投入する server replay、古い行を消す server purge と RETENTION_DAYS、Prometheus 形式の /metrics、リクエスト ID つきの JSON アクセスログ(ペイロード、ヘッダー、シークレットは出力しません)。
  • 安全なハンドラー。 worker.Permanent(err) を返すと、何度やっても成功しないペイロードを、リトライを消費せずに退避できます。ハンドラーの実行中にシャットダウンしても、試行回数を減らさずにイベントをキューへ戻します。
  • サンプル。 Polar の order.paid ハンドラーと汎用の JSON ルーターがあり、どちらもテスト付きです。Dockerfile と systemd ユニットも入っています。

pure Go(cgo なし)です。依存は標準ライブラリと modernc.org/sqlite だけです。

計測結果

方法は次のとおりです。付属の負荷テスター(make loadtest)を、コンパイル済みバイナリに対して Apple Silicon のノート PC で実行しました。接続数 32、ペイロード 512 バイト、ループバック上の実 HTTP、既定の耐久性設定(WAL、synchronous=FULL)、一回の実行で 5,000 イベント、実行は三回です。

項目結果
HTTP で受理できたイベント数(検証、永続化つきの挿入、202)、GitHub 形式毎秒約 7,200〜9,200(p50 2.7 ms、p99 約 17 ms)
同じく Standard Webhooks毎秒約 9,400
最初のリクエストからキューが空になるまで(何もしないハンドラー)、ワーカー 1毎秒約 3,100〜3,200
キューの消化、5 ms スリープするハンドラー(ネットワーク呼び出し相当)、ワーカー 1 / 8 / 32毎秒約 163 / 1,463 / 5,264

主なコストは HTTP ではなくデータベースです。SQLite へのアクセスはすべて一本の接続を通ります。そのため、ワーカーを増やして効果が出るのは、ハンドラーがネットワーク呼び出しや別のデータベースなど、何かを待つ場合です。

どうテストしたか

make test は go vet と、レースディテクター付きの 41 テストを実行します。対象は、改ざんされた、古い、未来の、不正な形式の署名、リファレンス Go ライブラリにある Standard Webhooks の既知解ベクター、シークレットのローテーション、401 のとき何も保存しないこと、重複配信、サイズ超過とメソッド違いのリクエスト、リトライとバックオフ、デッドレター、クラッシュからの復旧、順序、多数のワーカーが同じイベントを二度処理しないこと、リプレイ、パージ、0.1.0 のデータベースからのアップグレードです。パッケージ化した zip からビルドしたバイナリにも、三方式すべてで署名つきの実 HTTP リクエストを送って動作を確認しました。同じ配信のリプレイも含みます。

想定している使い手

Polar、Stripe、GitHub などから Webhook を受け取りたい開発者向けです。署名、リトライ、重複排除、クラッシュに耐えるキューといった、地味だけれど外せない部分をきちんと済ませたい方に向いています。

サーバー単体での利用を前提にしています。プロセスは一つ、SQLite への書き込みも一つで、イベントは既定で順番に処理されます。配信は at-least-once なので、ハンドラーは冪等にしてください。

よくある質問

Polar の Webhook を検証できますか?

はい。Polar は Standard Webhooks を使っており、これに対応しています。Polar の注文イベント用のサンプルハンドラーも付いています。

署名検証のコードだけ使えますか?

はい。MIT ライセンスの小さなモジュール webhooksig として別に配布しています。無料ツールのページをご覧ください。

CGO や C コンパイラーは必要ですか?

いいえ。SQLite ドライバーは pure Go です。

Go のバージョンは?

Go 1.24 以降です。

ライセンスはどうなっていますか?

ご自身のプロジェクトで使えます。競合するスターターとして、ソースを再販することはできません。

変更履歴

  • 0.2.1: SQLite ドライバーを v1.44.0 に更新(Go 1.24 以上が必要)。
  • 0.2.0: Standard Webhooks、シークレットのローテーション、複数ワーカー、replay と purge のコマンド、/metrics、アクセスログ、Polar のサンプル、計測値を追加。
  • 0.1.0: 初回リリース。

続きを読む

入手する

Polar で購入: $24

支払い、税、請求書は販売代行事業者の Polar が処理します。決済後にダウンロードリンクが届きます。