漏出去的字段与停住的循环
目标
亲手制造并修复 FastAPI 在实际工作中最常出现的两类问题。
- 响应中泄露敏感字段(第 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)。不要写任何一行验证代码——类型本身就是验证。执行 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,而是直接 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) 的路径——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。两段代码只有一个词(async)不同,耗时却相差 4 倍——前者在单个事件循环中排队,后者则由 FastAPI 送入线程池。本练习的结论是:如果没有把握,使用 def 更安全。
替换依赖项进行测试
编写 test_app.py,并至少包含一个使用 dependency_overrides 将 get_store 替换为假实现的测试。pytest -q 必须通过。
使用 from fastapi.testclient import TestClient 和 app.dependency_overrides[get_store] = lambda: {...}。无需修改一行生产代码就能替换依赖项,这才是 Depends 的真正价值。请勿使用猴子补丁。
总结这两类事故
在 09-notes.md 中写三行:(1)第 3 步泄露了什么;(2)第 7 步为什么会相差 4 倍;(3)分别使用什么方法阻止了这两个问题。
正文中必须包含 response_model 和 def 这两个词。它们对应 FastAPI 中最常出现的两类事故。