本番でStripeの決済テストをしたとき、購入完了後に記事が読めなかった。Stripeのダッシュボードを見ると「422 Unprocessable Entity」というエラーが記録されていた。
原因を特定して修正するまで2時間かかった。その過程を記録する。
エラーの構造
Webhookは「StripeのサーバーがこちらのAPIに対してPOSTリクエストを送る」という仕組みだ。このPOSTリクエストに含まれる情報を処理して、DBに購入記録を保存する。
422エラーはFastAPIのバリデーションエラーだ。「受け取ったリクエストの形式が期待通りでない」ときに返る。
最初の仮説は「StripeのWebhookデータの形式が想定と違う」だった。だが、Stripeのドキュメントを確認しても、形式に問題は見当たらない。
最初の原因:型アノテーションの欠落
FastAPIは関数のパラメータに型アノテーションがないと、リクエストボディをJSONとして解析しようとする。Webhookのエンドポイントはこう書いていた:
async def stripe_webhook(request):
request に型アノテーションがない。FastAPIはこの request を「リクエストボディのスキーマ」として扱おうとして、422を返していた。
修正は単純だった:
from fastapi import Request
async def stripe_webhook(request: Request):
Request 型を明示することで、FastAPIは「これはHTTPリクエストオブジェクトだ」と理解する。
これで422が解消するはずだった。しかし500エラーに変わった。
第二の原因:StripeオブジェクトはDictではない
次のエラーはPythonの AttributeError: get だった。
Webhookから届いたデータを処理するコードに、こう書いていた:
session = event["data"]["object"]
meta = session.get("metadata", {})
Stripeの新しいSDKでは session は dict ではなく StripeObject という独自クラスのインスタンスだ。StripeObject には .get() メソッドがない。
最初は dict(session.metadata) に変えたが、今度は KeyError: 0 が出た。StripeObject に対して dict() を呼ぶと、整数キーで反復しようとするためだ。
最終的な解決策は getattr() を使うことだった:
meta = session.metadata
user_id = getattr(meta, "user_id", None)
getattr() はオブジェクトの属性に安全にアクセスする。存在しなければデフォルト値を返す。
2時間で3つのバグを踏んだ理由
振り返ると、この2時間で踏んだバグは以下の3つだ:
requestの型アノテーション欠落 → 422エラーsession.get()がStripeObjectで使えない → 500エラーdict(session.metadata)がStripeObjectで正しく動かない → 500エラー
全て「StripeのSDKバージョンが上がって、返り値の型が変わった」ことが根本原因だ。古いドキュメントやサンプルコードを参考にすると、このような「バージョン違いのバグ」を踏む。
AIとデバッグしていると、ログを貼り付けるたびに「このエラーはこの行が原因です」という的確な指摘が返ってくる。しかし「なぜStripeObjectにgetメソッドがないのか」という根本を理解するのは人間の仕事だ。AIは症状を特定するのは得意だが、「このライブラリのこのバージョンから仕様が変わった」という文脈は持っていない場合がある。
デバッグの最後に試したのは「Python REPLでStripeObjectのメソッド一覧を確認する」という原始的な手法だった。
import stripe
m = stripe.StripeObject()
print(hasattr(m, 'get')) # False
これで「getメソッドは存在しない」と確認できた。コードの問題ではなく、オブジェクトの仕様の問題だと確定した瞬間から、解決策が見えた。
本番で起きていたらと思うと
テスト段階でこのバグを踏めた。本番で顧客が購入した後に記事が読めない状態が起きていたら、返金対応や信頼失墜につながっていた可能性がある。
Stripeの場合、Webhookが失敗すると自動的に再送する仕組みがある。修正後に「再送」ボタンを押したら、正しく処理されて購入記録がDBに登録された。
テスト環境で完璧に動作確認してから本番に出す——この原則の重要性を、改めて実感した2時間だった。
*次回は「記事コンテンツの設計——価格設定・アクセス制御・何を無料にするか」*
*シンプルシステム株式会社 代表 伊藤勝彦*