背景
- Stripeを利用したWebhookの受信処理を実装する機会があった
- 「なぜわざわざWebhook用のモデルを作るのか?コントローラーで直接処理すればよいのでは?」と疑問に思ったので調べた
- 調べてみると、モデル化には明確な理由があったので残しておく
Stripe Webhookとは
Webhookとは
特定のイベントが発生したときに、別のサービスやアプリケーションへリアルタイムで通知を送る仕組み
通常のAPIが「こちらから問い合わせる(ポーリング)」のに対して、Webhookは「あちらから通知が届く」という逆方向の通信になります
Stripe Webhookとは
Stripe上で決済完了やサブスクリプション更新などのイベントが発生した際、自動的に自社サーバーへHTTP POSTリクエストで通知する仕組みです
例えば、以下のようなイベントが通知されます
-
checkout.session.completed(決済完了) -
customer.subscription.updated(サブスクリプション更新) -
invoice.payment_failed(請求の支払い失敗)
コード
Webhook用のモデルを作成し、受信したイベントを保存→非同期処理する実装例
マイグレーション
class CreateStripeWebhooks < ActiveRecord::Migration[7.1]
def change
create_table :stripe_webhooks do |t|
t.string :event_id, null: false
t.json :body, null: false
t.timestamps
end
add_index :stripe_webhooks, :event_id, unique: true
end
end
モデル
class StripeWebhook < ApplicationRecord
validates :event_id, presence: true, uniqueness: true
after_create_commit :process_later
private
def process_later
StripeWebhookProcessJob.perform_later(id)
end
end
コントローラー
class StripeWebhooksController < ApplicationController
def create
if StripeWebhook.exists?(event_id: @event.id)
head :ok
return
end
StripeWebhook.create!(event_id: @event.id, body: @event)
head :ok
end
end
ジョブ
class StripeWebhookProcessJob < ApplicationJob
def perform(stripe_webhook_id)
stripe_webhook = StripeWebhook.find(stripe_webhook_id)
case stripe_webhook.body['type']
when 'checkout.session.completed'
# 決済完了時の処理
when 'customer.subscription.updated'
# サブスクリプション更新時の処理
end
end
end
処理の流れ
- Stripeからエンドポイントに対してHTTP POSTでイベントが送信される
- 同一イベントが既に存在する場合は
200 OKを返して終了する - 受信したイベントを
stripe_webhooksテーブルに保存し、200 OKを返す -
after_create_commitでActiveJobが起動し、バックグラウンドでイベントを処理する
Webhookをモデル化した方がいい理由
1. 信頼性の高い非同期処理(データの欠損を防ぐ)
WebhookはHTTP POSTで送られてきますが、その瞬間にサーバーがダウンしていたり、処理が失敗したりすると、イベントデータは消失してしまいます
stripe_webhooksテーブルにイベント情報を保存することで、処理に失敗した場合でも後から再処理(リトライ)が可能になります。決済の失敗やサブスクリプションの更新漏れといった致命的な問題を防げます
なお、Stripeは本番環境では指数バックオフで最大3日間イベントの再送を行いますが、3日間すべて失敗し続けるとエンドポイントが自動的に無効化されます。自社DB側にイベントを保存していれば、Stripe側のリトライに依存せず自前でリカバリーできます
2. 冪等性(べきとうせい)の担保
Stripeは、Webhookが正常に処理されたと認識できるまで、同じイベントを何度も再送してきます
受信したイベントのid(evt_で始まる一意の識別子)をテーブルに保存し、ユニーク制約をかけておくことで、同じイベントの二重処理を防止できます
例えば、1回の支払いに対して誤って2回分のサブスクリプションを登録してしまう、といったバグの防止につながります
上記のコード例では、exists?で重複チェックを行い、既に存在する場合は200 OKを返して処理をスキップすることで、Stripe側のリトライを正常に停止させています
3. 非同期処理によるパフォーマンス向上
Webhookの処理が重い場合、サーバーから即座にレスポンスを返さないとStripe側でタイムアウトとみなされます
コントローラーでは「イベントをデータベースに保存して200 OKを返す」ことだけ行い、実際の処理(メール送信、プラン更新など)はafter_create_commitからActiveJobで非同期に実行します
これによってレスポンスを早めています
4. 監査・デバッグの容易さ
「いつ、どのイベントが届いたか」がデータベースに履歴として残ります
顧客から「支払ったのにサービスが使えない」と問い合わせを受けた際、stripe_webhooksテーブルを見れば「イベントは届いたが処理に失敗した」のか「そもそもイベントが届いていない」のかがすぐに特定できます
5. データの一貫性(ローカルコピーの維持)
Stripe側の状態(サブスクリプション状況など)を自社DBに同期しておくことで、Stripe APIへの問い合わせを最小限に抑えられます
画面表示のたびにStripe APIを叩く必要がなくなるため、レスポンス速度の向上やAPIレート制限の回避にもつながります
感想
- シンプルにWebhook用のモデルがあった方がコードの見通しがよくなるなぁというのはかなり大きい理由
- コントローラーは「受け取って保存して返す」だけ、モデルは「イベントの種類に応じて処理する」だけ、と責務が明確に分かれるので、後から見ても何をやっているかが分かりやすくなります
参考URL