一覧SD-01-1 · 入力検証と解釈の境界

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・標準パーサー / 固定の要求

1
from sd01 import API api = API() print(api.state())
(5, [])
2
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, [])
3
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, [])
4
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, [])
5
print(api.post({"name":"book","quantity":2})) print(api.state())
(201, {'quantity': 2, 'total': 200})
(3, [('book', 2, 200)])
6
print(api.post({"name":"book","quantity":4})) print(api.state())
(409, {'error': 'stock'})
(3, [('book', 2, 200)])
7
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)])
8
print(api.post({"name":"book","quantity":3})) print(api.state()) api.close()
(201, {'quantity': 3, 'total': 300})
(0, [('book', 2, 200), ('book', 3, 300)])

演習完了