Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】stripe told me my webhook was broken. i found the email 17 days later.

2026年10月01日に「Dev.to」が公開したITニュース「stripe told me my webhook was broken. i found the email 17 days later.」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

StripeのWebhookエラーメール発見が17日遅れた。Next.js移行時の設定ミスでWebhookが機能せず、サブスクリプションの解約処理が不能に。期限切れユーザーが永久無料でサービスを利用できる状態だった。日付管理のバグも発覚し、正確な設定と定期チェックの重要性を痛感した。

ITニュース解説

今回の出来事は、オンラインサービスで利用される決済プラットフォームStripeからの重要な通知(Webhook)が長期間機能せず、システムに深刻な問題を引き起こした事例である。サービス提供者は約10週間にわたりStripeからの決済やサブスクリプションに関する通知を受け取れておらず、この問題に気づいたのは17日前に届いたStripeからの警告メールを発見した後だった。この状況は、システム構築や運用における重要な教訓を示すものだ。

まず、Webhookの基本的な役割を理解する。Webhookとは、Stripeで特定のイベント(例えば、ユーザーがサービスに課金した、サブスクリプションが更新された、またはキャンセルされたなど)が発生した際に、その情報を自動的にサービス側のシステムにHTTPリクエストとして通知する仕組みだ。これにより、サービスシステムはStripeで発生したイベントをリアルタイムで検知し、ユーザーのプレミアム機能の有効化や解約処理といった適切な処理を実行できる。今回の問題は、このStripeとサービスシステム連携の要となるWebhookが機能していなかったために発生した。

Webhookが機能しなくなった根本原因は、システムのバックエンドをExpressからNext.jsへ移行したことにあった。この移行作業に伴い、環境変数の設定にミスがあったのだ。環境変数とは、APIキーやデータベース接続情報など、アプリケーションの動作に必要な設定値をコードとは別に管理するもので、一般的に.envファイルに記述される。古いExpressベースのシステムでは、ルートディレクトリに配置された.envファイルからStripeの秘密鍵などの情報を読み込んでいた。しかし、Next.jsベースの新システムがweb/というサブフォルダ内でpm2というプロセス管理ツールを使って実行される設定になった際、新システムはweb/.envというファイルを読み込むように構成されていた。そして、このweb/.envファイルにStripe関連の重要な変数が全く記述されていなかったのだ。

そのため、StripeからWebhookが送信されるたびに、サービス側のシステムは秘密鍵を見つけることができなかった。秘密鍵がない状態では、Webhookの署名検証(送信元が本当にStripeであるかを確認するセキュリティチェック)に失敗し、システムは不正なリクエストとして処理してしまった。具体的には、エラーハンドリングのコードが「Stripe」という文字列を含むエラーメッセージをすべて400番(不正なリクエスト)として扱うようになっていたため、システム側からは認証失敗も設定ミスも同じ「不正なリクエスト」としか認識されなかった。Stripe側もWebhook送信が失敗し続けるため、最終的にエンドポイントを無効化してしまった。これは、システムの内部設定の問題が、外部からはまるでセキュリティエラーのように見える典型的なパターンだ。

さらに、.envファイルには別の設定ミスもあった。環境変数をNAME = valueのように、イコール記号の周りにスペースを入れて記述していたため、dotenvライブラリがこれを「NAME 」という変数名(末尾にスペースを含む)として誤って認識してしまっていたのだ。たとえ正しい.envファイルが読み込まれたとしても、意図した変数名で値を取得しようとするとundefined(未定義)となり、設定は機能しなかっただろう。これは、設定ファイルのわずかな書式ミスがシステム全体の動作に影響を与える例を示している。

Webhookの故障は、さらに深刻な問題を引き起こした。サービス提供のプレミアムプランの終了処理は、Stripeから送られるcustomer.subscription.deletedというWebhookイベントに完全に依存していた。つまり、このWebhookイベントを受け取ることでのみ、ユーザーのプレミアムステータスを「アクティブではない」と更新する仕組みだった。Webhookが機能しなかったため、Stripe側でサブスクリプションが終了しても、サービス側のシステムはそれを検知できず、誰もプレミアムプランを解約できない状態になっていた。結果として、一度プレミアムプランに加入したユーザーは、Stripeでの支払いが停止しても、サービス上では永久にプレミアム機能を利用できる状態が続いていたのだ。

この問題は、データベースの設計と運用における不備も露呈させた。ユーザーのプレミアムプランの終了日時を記録するexpires_atというカラムがデータベースに存在し、インデックスも設定されていたにもかかわらず、このexpires_atカラムの値はアプリケーションのコードのどこでも読み込まれていなかった。重要な情報が保存されているのに、それが使われていなかったのである。

さらに、expires_atとcurrent_period_endという二つの日付カラムが存在し、これらは通常、互いに同じ日付を指すべきであった。しかし、古いデータの中にはexpires_atが過去の日付を示しているにもかかわらず、current_period_endがNULL(空っぽ)になっているものが複数見つかった。サービス提供のAI機能の利用制限コードは、このcurrent_period_endがNULLである場合を「生涯プレミアム利用権」と誤認するように実装されていたため、実際にはすでに終了しているはずのユーザーに、無期限でAIのプレミアム機能を提供してしまっていた。

これらの問題に対する修正は多岐にわたる。まず、アクセス制御(ゲート)のロジックが強化された。ユーザーがサービスを利用する際に、expires_atとcurrent_period_endのどちらか一方でも日付が過去を示していれば、プレミアムアクセスを拒否するように変更された。ただし、両方のカラムがNULLの場合は、Stripe以外の経路で付与された生涯無料利用権などの例外的なケースを考慮し、引き続きプレミアムアクセスを許可する。また、日付の書式が不正で解析できない場合は、ユーザーをロックアウトしないよう、プレミアムアクセスを許可する「フェイルオープン」の考え方を取り入れた。これは、システム側のバグによって正規のユーザーがサービスを利用できなくなる事態を防ぐための措置だ。

最も重要な修正の一つは、日次で実行される「スイープ処理」の導入である。これは、毎日決まった時間にすべてのユーザーのサブスクリプション情報をチェックし、日付が過ぎているにもかかわらず「アクティブ」とされているアカウントがあれば、そのステータスを自動的に解除するバッチ処理だ。この処理は、Webhookが一時的に機能しなくなったとしても、最終的にデータの一貫性を保ち、不正なプレミアム利用を防ぐためのセーフティネットとなる。実際にこのスイープ処理をテストした結果、過去に解約されたはずなのにプレミアムが付与され続けていた2つのアカウントが正しく無効化され、生涯利用権を持つアカウントには影響がなかったことが確認された。

この出来事から得られる教訓は多い。まず、外部サービスとの連携(Webhookなど)だけに頼るのではなく、システム内部で有効期限をチェックするロジックを必ず実装することの重要性だ。イベント駆動型のシステムは強力だが、イベントの受信に失敗する可能性は常にあるため、二重、三重のチェック体制を設けることが不可欠だ。また、データベースに保存された重要なデータ(expires_atのような終了日時)は、単に保存するだけでなく、実際にアプリケーションのロジックで適切に読み込まれ、利用されているかを確認する必要がある。最後に、決済プラットフォームなどからの重要な通知メールや警告は、たとえ多数届く中に埋もれていても、定期的に確認し、迅速に対応することが、予期せぬ大きな問題を防ぐ上で極めて重要である。

関連コンテンツ

関連IT用語

関連ITニュース