全国オンライン対応受付 平日 9:00-18:00
お問い合わせ

開発記録 / 1cto.jp制作記 / Vol.6 サイト構築・Stripe実装編

記事 03

Webhook署名検証で422エラーが出続けた2時間の記録

本番でStripeの決済テストをしたとき、購入完了後に記事が読めなかった。Stripeのダッシュボードを見ると「422 Unprocessable Entity」というエラーが記録されていた。

2026-06-02 公開

本番で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では sessiondict ではなく 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つだ:

  1. request の型アノテーション欠落 → 422エラー
  2. session.get() がStripeObjectで使えない → 500エラー
  3. 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時間だった。


*次回は「記事コンテンツの設計——価格設定・アクセス制御・何を無料にするか」*

*シンプルシステム株式会社 代表 伊藤勝彦*