漏れるフィールドと止まるループ
目標
実務でFastAPIで最もよく起きる2つの問題を、自分で起こして直します。
- レスポンスに秘密のフィールドが漏れること(ステップ3 → 4)
async defの中のブロッキングが、サーバー全体を止めること(ステップ7)
ルール
- ファイルは、すべて
/root/work/apiに作ります。 - 名前は次のとおりにしてください。アプリのインスタンス:
app、ストアの依存性:get_store。採点がその名前で読み込みます。 - 採点は、皆さんの
app.pyを直接読み込んで、リクエストを入れてみます。uvicornが起動していなくても採点されます(ステップ1だけは例外で、実際に起動する必要があります)。
サーバーを起動する
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のあとで起動し直すほうが、何が動いているのかが明確です。
ステップ
/healthz→01-healthz.txt- Pydanticの検証、422 →
02-422.json - わざと漏洩させる →
03-leak.json response_modelでブロックHTTPException404Depends(get_store)async defvsdef→07-block.txt・07-unblock.txtdependency_overridesのテスト- まとめ →
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で最もよく起きる事故です。