個人開発でぶつかった「StripeObjectは辞書じゃない」問題
個人で運営しているプログラミング学習サイト「CodeQuest」に、複数の料金プラン(月額・3ヶ月・半年・年間)とクーポン機能を実装したときの話です。実装中、同じパターンのエラーに何度も足を取られました。今回はその顛末を共有します。
最初のエラー
決済完了後、ユーザーのプラン状態を更新する処理を書いていました。クーポンコードをメタデータから取り出すコードはこうなっていました。
cpn_code = (cs.metadata or {}).get('coupon_code', '')
一見、何も問題なさそうに見えます。cs.metadataが空ならデフォルトで空の辞書を使い、そこから.get()で安全に値を取り出す、よくあるパターンです。
ところが、本番環境でこの行が動くたびに、こんなエラーが記録されていました。
AttributeError: get
原因:StripeObjectは辞書ではない
調べてみると、Stripeの SDK が返すmetadataは、Python のdictではなくStripeObjectという独自の型でした。属性アクセス(cs.metadata.foo)はできるものの、辞書のメソッド(.get())はサポートしていない場面があったのです。
さらにやっかいだったのが、(cs.metadata or {})という書き方です。StripeObjectの空インスタンスは、Python的には「真」と評価されるため、or {}によるフォールバックが効きません。つまり、空であっても{}に置き換わらず、そのままStripeObjectが使われ続けていたのです。
解決策
最終的に、もっとも安全だったのはgetattr()を使う方法でした。
cpn_code = getattr(cs.metadata, 'coupon_code', '') if cs.metadata else ''
getattr()は辞書変換やメソッド呼び出しを一切行わず、属性として直接アクセスします。StripeObjectはキーを属性としてアクセスできる設計になっているため、内部実装に依存せず確実に動きます。
途中でdict(cs.metadata)という変換も試しましたが、これもKeyError: 0という別のエラーで失敗しました。StripeObjectをdict()に渡すと、内部的に配列のようなインデックスアクセスが発生してしまうことがあるようです。結局、シンプルなgetattr()が一番確実でした。
もう1つのハマりポイント:current_period_end の場所
サブスクリプションの解約処理でも、似た問題に遭遇しました。「次回更新日」を取得しようとしてsub.current_period_endにアクセスしたところ、同じAttributeErrorが出ました。
実際にAPIレスポンスをそのままログ出力して構造を確認したところ、このフィールドはサブスクリプションオブジェクトのトップレベルにはなく、items.data[0].current_period_endの中にありました。一方、cancel_atというフィールドはトップレベルに存在していて、こちらの方が今回の用途には都合が良いこともわかりました。
try:
cancel_at = None
cancel_at_ts = getattr(sub, 'cancel_at', None)
if cancel_at_ts:
cancel_at = datetime.utcfromtimestamp(cancel_at_ts)
except Exception:
cancel_at = None
学んだこと
- 外部APIのレスポンスを「辞書のようなもの」として扱うのは危険。型を確認してから処理を書く
- 推測で直すより、実際のレスポンスをそのままログ出力して構造を確認するのが一番早い
or {}のようなフォールバックは、対象の型によっては効かないことがある
同じようなエラーに遭遇している方の参考になれば幸いです。
コメント(0件)
まだコメントはありません。最初のコメントを投稿してみませんか?
コメントを投稿する
承認後に公開されます。