APIの入力検証
構文・型・長さ・業務条件の検査を、HTTP応答とデータベースの状態へ対応付けます。
説明
入力検証では、JSONとして読めること、型と長さが仕様に合うこと、業務上その操作を実行できることを順に確認します。ブラウザ側で入力欄を制限していても、サーバー側で受け取る値の検証は必要です。
商品7を注文する小さなHTTP APIを使います。整数と文字列、数量の上下限、名前の長さ、余分な項目、在庫不足を比較し、拒否した要求が保存データを変えていないか確認します。
構文・型・長さ・業務条件の検査を、HTTP応答とデータベースの状態へ対応付けます。
1注文APIと初期在庫を確認する
名前はNFC正規化後1〜4コードポイント、数量は整数1〜5です。単価100はサーバー側で持ち、開始時の在庫は5個です。
from sd01 import API
api = API()注文APIと独立したデータベースを起動する
print(api.state()在庫と保存済み注文を確認する)在庫5、注文0件です。文字数は見た目の文字やUTF-8バイト数ではなく、このAPIが定めたコードポイント数です。
2JSONの型を取り違えた入力
数量の文字列と真偽値を送ります。Pythonではboolがintの派生型なので、isinstanceだけの検査では真偽値を受け入れることがあります。
print(api.post({"name":"book","quantity":"2"整数の代わりに文字列を送る}))
print(api.post({"name":"book","quantity":True真偽値を送る}))
print(api.state()型エラー後の保存状態を確認する)どちらも422のquantity_typeで、在庫は5のままです。このAPIはtype(value) is intで真偽値を除いています。
3数量の上下限を検査する
0個と6個は、このAPIの受付範囲1〜5の外です。
print(api.post({"name":"book","quantity":0下限未満を送る}))
print(api.post({"name":"book","quantity":6上限超過を送る}))
print(api.state())quantity_rangeで拒否し、注文も増えません。負の数量で在庫や金額が逆方向へ動く処理も、範囲検査で防ぐ必要があります。
4名前の長さと余分な項目を検査する
5文字の名前と、クライアント指定の単価を含む要求を比較します。
print(api.post({"name":"books"文字数の上限を超える,"quantity":1}))
print(api.post({"name":"book","quantity":1,"price":1受付仕様にない単価を追加する}))
print(api.state())name_lengthとfieldsです。未知の項目を自動でモデルへ代入せず、業務値の単価をクライアントに決めさせていません。
5正しい注文を保存する
境界ちょうど4文字のbookを2個注文します。
print(api.post({"name":"book","quantity":2})正しい型と範囲の注文を送る)
print(api.state())201、合計200で、在庫は3です。検証後の値から単価と数量を掛け、注文と在庫更新を同じトランザクションで確定しています。
6形式が正しくても在庫不足になる
数量4は受付範囲内ですが、残り在庫は3です。
print(api.post({"name":"book","quantity":4受付範囲内だが在庫を超える数量を要求する}))
print(api.state())409のstockで、在庫3と既存注文1件を保ちます。SQLの条件付きUPDATEが在庫以上の消費を防いでいます。
7構文とサイズのエラーを比較する
JSON構文が不正な要求、Content-Typeが違う要求、128バイトを超える本文を送ります。
print(api.post("{")JSON構文エラーを送る)
print(api.post({"name":"book","quantity":1},"text/plain"異なるメディア型を指定する))
print(api.post("x"*129本文サイズの上限を超える))
print(api.state())400・415・413で拒否し、保存状態は同じです。Content-Lengthが上限を超える場合は、本文を読み込む前に拒否して接続を閉じます。
8残り在庫ちょうどを注文する
残り3個を注文し、在庫0を確認します。
print(api.post({"name":"book","quantity":3残り在庫ちょうどを要求する}))
print(api.state()全要求後の確定状態を確認する)
api.close()二つの注文の数量は2+3=5、合計額は200+300=500です。各拒否ケースが在庫や注文を増減させていなかったことも確認できます。
まとめ
JSON構文、型、長さ、業務条件を段階的に検査します。
拒否応答と保存状態をセットで確認します。
業務条件は更新する時点でも満たすよう、DBの条件付き更新を使います。
操作履歴と解説を振り返れます。
Python 3.15 / HTTP・SQLite・標準パーサー / 固定の要求
from sd01 import API
api = API()
print(api.state())(5, [])
print(api.post({"name":"book","quantity":"2"}))
print(api.post({"name":"book","quantity":True}))
print(api.state())(422, {'error': 'quantity_type'})
(422, {'error': 'quantity_type'})
(5, [])print(api.post({"name":"book","quantity":0}))
print(api.post({"name":"book","quantity":6}))
print(api.state())(422, {'error': 'quantity_range'})
(422, {'error': 'quantity_range'})
(5, [])print(api.post({"name":"books","quantity":1}))
print(api.post({"name":"book","quantity":1,"price":1}))
print(api.state())(422, {'error': 'name_length'})
(422, {'error': 'fields'})
(5, [])print(api.post({"name":"book","quantity":2}))
print(api.state())(201, {'quantity': 2, 'total': 200})
(3, [('book', 2, 200)])print(api.post({"name":"book","quantity":4}))
print(api.state())(409, {'error': 'stock'})
(3, [('book', 2, 200)])print(api.post("{"))
print(api.post({"name":"book","quantity":1},"text/plain"))
print(api.post("x"*129))
print(api.state())(400, {'error': 'json'})
(415, {'error': 'content_type'})
(413, {'error': 'body_size'})
(3, [('book', 2, 200)])print(api.post({"name":"book","quantity":3}))
print(api.state())
api.close()(201, {'quantity': 3, 'total': 300})
(0, [('book', 2, 200), ('book', 3, 300)])演習完了