FastAPI 系列
状态码和异常为什么不要裸 raise(当前篇)
客户端不读你的 traceback。它们读状态码和 JSON detail。裸 raise Exception("没找到") 会变成 500,还会把内部信息打进日志甚至响应。业务失败要翻译成 HTTPException 或注册过的异常处理器。

用 HTTPException 表达协议错误
from fastapi import FastAPI, HTTPException, status
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id: int):
item = store.get(item_id)
if item is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Item not found",
)
return item
401 还要带 headers={"WWW-Authenticate": "Bearer"},浏览器和部分客户端才知道走认证。403 是「身份已知但不允许」,不要和 401 混用。
校验错误已经有 422
Pydantic 失败由框架处理,不要在路由里把模型校验再包一层 400。400 留给「语法对、业务不允许」,例如库存不足。
领域异常映射到 HTTP
class ItemNotFoundError(Exception):
def __init__(self, item_id: int):
self.item_id = item_id
@app.exception_handler(ItemNotFoundError)
async def item_not_found_handler(request, exc: ItemNotFoundError):
return JSONResponse(
status_code=404,
content={"detail": f"Item {exc.item_id} not found"},
)
服务层抛领域异常,Web 层翻译。这样服务层可以给 CLI 或后台任务复用,不必依赖 FastAPI。
不要把内部异常细节返回给客户端
生产关闭 traceback 响应。日志可以记完整堆栈,响应只给稳定的 detail。数据库报错原文、文件路径、SQL 都不要出现在 JSON 里。
常见误区
用 200 包装错误
{"code": 404, "msg": "没有"} 配 HTTP 200,会让网关、监控和 TestClient 全部失效。HTTP 状态码是协议的一部分。
catch Exception 后静默返回空对象
故障被吞掉,监控看不到。未知错误应成为 500 并报警。
404 和 403 用同一个文案
对隐私敏感的资源,有时故意统一成 404,避免探测是否存在。这是产品决策,要写进接口约定,而不是随手混用。
小结
状态码是给机器读的契约。业务失败走 HTTPException 或 handler,系统失败才是 500。下一篇把 401/403 接到 Depends 上的 JWT。
参考资料
状态码和异常为什么不要裸 raise
https://lautung.com/archives/fastapi-08-exceptions
评论