TT Lab
はじめる
学ぶ 学習パス コース

FastAPI — 型がそのまま契約だ

漏れるフィールドと止まるループ

TT Labで続きを見る

目標

実務でFastAPIで最もよく起きる2つの問題を、自分で起こして直します。

ルール

サーバーを起動する

cd /root/work/api
uvicorn app:app --host 0.0.0.0 --port 8000 > /tmp/uv.log 2>&1 &
curl -s localhost:8000/healthz

--reloadは使わないでください。ファイルを直したら、kill %1のあとで起動し直すほうが、何が動いているのかが明確です。

ステップ

  1. /healthz → 01-healthz.txt
  2. Pydanticの検証、422 → 02-422.json
  3. わざと漏洩させる → 03-leak.json
  4. response_modelでブロック
  5. HTTPException 404
  6. Depends(get_store)
  7. async def vs def → 07-block.txt・07-unblock.txt
  8. dependency_overridesのテスト
  9. まとめ → 09-notes.md

参考

検証のコードを手で書かないでください。if not isinstance(...)を書いているなら、型ヒントの仕事を奪っています。

サーバーを起動する

/root/work/api/app.pyにFastAPIアプリを作って、GET /healthzが{"status":"ok"}を返すようにしてください。uvicornで実際に起動して、curlした結果を01-healthz.txtに残します。

mkdir -p /root/work/api && cd /root/work/api。app = FastAPI()は、必ず名前がappでなければなりません。採点がその名前で読み込みます。起動: uvicorn app:app --host 0.0.0.0 --port 8000 > /tmp/uv.log 2>&1 &、続いてcurl -s -i localhost:8000/healthz > 01-healthz.txt。

不正な入力を拒否させる

POST /itemsを作って、本文をPydanticのモデルで受け取ってください。モデルにはname: strとqty: intが必要です。qtyに文字列を送って422を受け取り、その本文を02-422.jsonに残してください。

class ItemIn(BaseModel): name: str; qty: int、そしてdef create(item: ItemIn)。検証のコードは1行も書かないでください。型がそのまま検証です。curl -s -X POST localhost:8000/items -H 'content-type: application/json' -d '{"name":"a","qty":"many"}' > 02-422.json。

秘密のフィールドを、わざと漏らしてみる

GET /meを作って、emailとhashed_passwordを両方持つオブジェクトをそのまま返すようにしてください。レスポンスにハッシュがそのまま出ることを、03-leak.jsonに残します。このステップの正解は、漏洩です。

response_modelなしで、dictやモデルをそのままreturnすると、すべて出力されます。これが実際によく起きる事故で、次のステップで防ぎます。

出力モデルで防ぐ

GET /me/safeを追加して、response_modelでhashed_passwordがレスポンスから消えるようにしてください。ハンドラーは、今もオブジェクト全体を返してもかまいません。

出力専用のモデル(UserOut)には、emailだけを置きます。@app.get("/me/safe", response_model=UserOut)。ハンドラーのコードを直さなくても、フィールドがフィルタリングされることが核心です。入力モデルと出力モデルを、絶対に同じクラスで使いません。

ないものには404を返す

GET /items/{item_id}を作って、存在しないidには、404と一緒に、人が読めるメッセージを返すようにしてください。

raise HTTPException(status_code=404, detail="...")。return {"error": ...}で200を返してはいけません。ステータスコードは、契約の一部です。

依存性でストアを注入する

Dependsで共通のストアを注入するように変えてください。ストアを作る関数の名前はget_storeにします。

def get_store(): ...を作って、ハンドラーでstore = Depends(get_store)として受け取ります。グローバル変数を直接参照しないでください。次のステップで、これをまるごと差し替えます。

イベントループを止めてみて、直す

同じくtime.sleep(0.5)をする2つの経路を作ってください。GET /slow: async def、GET /slow2: def。それぞれ4つを同時に投げて、かかった時間を07-block.txtと07-unblock.txtに残します。前者は2秒、後者は1秒以内である必要があります。

測定: time (for i in 1 2 3 4; do curl -s localhost:8000/slow & done; wait) 2>&1 | tee 07-block.txt。コードは1文字(async)を除いて同じなのに、4倍の差が出ます。前者はイベントループ1つに行列を作ったもので、後者はFastAPIがスレッドプールに送ったものです。自信がなければdefで書くほうが安全だというのが、このラボの結論です。

依存性を差し替えてテストする

test_app.pyを書いて、dependency_overridesでget_storeを偽のものに差し替えたテストを、少なくとも1つ含めてください。pytest -qが通る必要があります。

from fastapi.testclient import TestClient、app.dependency_overrides[get_store] = lambda: {...}。プロダクションのコードを1行も直さずに差し替えられることが、Dependsの本当の価値です。モンキーパッチは使わないでください。

2つの事故をまとめる

09-notes.mdに3行を書いてください。(1)ステップ3で何が漏れたか、(2)ステップ7でなぜ4倍の差が出たか、(3)それぞれを何で防いだか。

response_modelとdefの2つの語が、本文に入っている必要があります。この2つが、FastAPIで最もよく起きる事故です。