response-handling-status-codes
📂 所属阶段:第一阶段 — 快速筑基(基础篇)
🔗 相关章节:path-query-parameters · request-body-handling
在构建 Web API 的时候,请求只是故事的一半,响应才是真正交付给客户端的东西。FastAPI 提供了非常灵活的响应控制能力,从最简单的数据返回,到状态码、响应头、exception-handling、甚至完全自定义的响应类型,都能轻松掌握。这篇文章把所有关键知识点串起来,让你一步到位学会「响应处理」。
一、响应模型(Response Model)
基础实现与字段过滤
很多时候,我们接收的请求数据比最终返回的数据要多。比如用户注册接口接收 password,但响应中绝不能把密码再回传给前端。FastAPI 用 response_model 参数来声明「返回契约」,自动过滤掉不该出现的字段。
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
# 最终返回给前端的用户信息
class PublicUser(BaseModel):
id: int
name: str
email: str
# 注册时接收的用户数据(含密码)
class UserCreate(BaseModel):
name: str
email: str
password: str
@app.post("/users/", response_model=PublicUser)
async def register(user: UserCreate):
# 模拟创建用户,密码会被自动过滤
return {"id": 1, **user.model_dump()}
上面这段代码中,虽然 return 里包含了 password,但因为 response_model=PublicUser,FastAPI 会用 PublicUser 模型把输出数据「清洗」一遍,只留下 id、name 和 email。对前端来说,密码从未暴露。
模型的核心价值
别以为响应模型只是「过滤字段」那么简单,它还能带来三大收益:
- 数据转换与校验:数据库返回的
Decimal 字段可以自动转成 JSON 友好的 float;缺失的可选字段会补全默认值;字段类型不对还会直接报错,防止脏数据流出。
- 自动生成 Swagger / OpenAPI 文档:前端开发对着文档一看,就能了解字段类型、必填项、示例值,沟通成本直线下降。
- 开发契约:前后端共享 Pydantic 模型,一旦接口字段变动,双方必须同步更新模型,安全性更高。
灵活的字段控制
除了用独立的响应模型,FastAPI 还提供三个参数,让你在单个接口里灵活调整输出的字段:
response_model_exclude:排除某些字段
response_model_include:只包含指定字段(优先级最高)
response_model_exclude_unset:排除未显式赋值的字段
from fastapi import status
# 1. 返回用户但隐藏邮箱
@app.post("/users/", response_model=PublicUser, response_model_exclude={"email"})
async def register_hide_email(user: UserCreate):
return {"id": 1, **user.model_dump()}
# 2. 只返回已设置的字段(未赋值的字段会被忽略)
@app.get("/users/{user_id}", response_model=PublicUser, response_model_exclude_unset=True)
async def get_user(user_id: int):
# 假设缓存中只有 id 和 name
return {"id": user_id, "name": "张三"}
# 3. 只返回商品的名字和价格
@app.get("/items/{item_id}", response_model=Item, response_model_include={"name", "price"})
async def get_item_summary(item_id: int):
return get_full_item_from_db(item_id)
当 response_model_exclude_unset=True 时,即使模型定义了 email 字段,但只要返回字典里没给它显式赋值,它就不会出现在响应里。这样就能灵活实现「局部更新」后的数据回包。
二、状态码(Status Code)
统一风格的设置方式
用状态码告诉客户端发生了什么是 RESTful 的基本礼仪。FastAPI 有两种写法:
- 直接写数字(小巧但不易维护)
- 使用
fastapi.status 常量(有 IDE 自动补全,语义清晰,强烈推荐)
from fastapi import status
@app.post("/items/", status_code=201)
async def create_item_raw(item: ItemCreate):
return {"id": 1, **item.model_dump()}
# 推荐方式:用常量
@app.delete("/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_item_pro(item_id: int):
# 204 规范要求响应体为空,直接 return 或 return None
return
返回 204 意味着删除成功,但响应体没有任何内容,浏览器也不会跳转页面。
高频状态码速查表
记住下面这些状态码,日常开发基本够用:
💡 小技巧:遇到客户端传参错误,可以返回 400;数据校验失败则交给 FastAPI 自动返回 422;权限问题用 403,防止攻击者猜出资源是否存在的用 404 遮掩。
三、直接操作 Response 对象
当简单的 response_model 和 status_code 满足不了需求时(例如动态修改响应头、设置 Cookie),你可以直接使用 Response 或它的子类。
完全自定义 JSON 响应
JSONResponse 是自定义 JSON 响应的利器,它能让你临时改变状态码、添加业务错误头:
from fastapi.responses import JSONResponse
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id > 100:
return JSONResponse(
status_code=status.HTTP_404_NOT_FOUND,
content={"code": "ITEM_NOT_FOUND", "message": "商品不存在"},
headers={"X-Error-Code": "ITEM_NOT_FOUND"}
)
return {"item_id": item_id, "name": "键盘"}
这里虽然返回了 JSONResponse,但 FastAPI 完全不会再用 response_model 去二次处理,所以你可以自由掌控响应的一切细节。
配置响应头与 Cookie
除了直接返回 JSONResponse,你还可以把 Response 对象作为参数注入,这样在正常返回 Pydantic 模型的同时,给响应加上额外的头信息:
from fastapi import Response
@app.get("/items/")
async def list_items(response: Response):
response.headers["X-Total-Count"] = "500"
return [{"id": 1, "name": "键盘"}]
@app.post("/login/")
async def login(response: Response):
# httponly=True 防止前端 JS 读取,减少 XSS 风险
response.set_cookie(
key="session_id",
value="secure_token_123",
httponly=True,
max_age=1800, # 30 分钟过期
samesite="lax" # 防止 CSRF 攻击
)
return {"message": "登录成功"}
需要留意的是:一旦你返回 Response 对象,FastAPI 就不会再对数据做任何序列化和模型校验。如果既要正常返回模型又要加响应头,记得用「注入 Response 参数 + 直接 return 字典或模型」的方式。
四、切换不同的响应类型
默认情况下 FastAPI 返回 JSON,但你完全可以通过 response_class 参数返回 HTML、纯文本、文件、甚至流式数据。
from fastapi.responses import (
HTMLResponse,
PlainTextResponse,
FileResponse,
StreamingResponse,
RedirectResponse
)
import io
# 1. HTML 页面
@app.get("/html/", response_class=HTMLResponse)
async def get_home():
return """
<html>
<head><title>我的小站</title></head>
<body><h1>Hello FastAPI 🚀</h1></body>
</html>
"""
# 2. 纯文本(健康检查)
@app.get("/text/", response_class=PlainTextResponse)
async def get_health():
return "OK"
# 3. 文件下载(自动设置 Content-Disposition 头)
@app.get("/download/report")
async def download_report():
return FileResponse(
path="./static/report.pdf",
filename="2024年度报告.pdf",
media_type="application/pdf"
)
# 4. 流式返回(适合大文件或实时日志)
@app.get("/stream/logs")
async def stream_logs():
async def generate_logs():
for i in range(10):
yield f"[INFO] 第 {i} 条日志\n"
return StreamingResponse(generate_logs(), media_type="text/plain")
# 5. 重定向
@app.get("/old-docs")
async def redirect_to_new_docs():
return RedirectResponse(url="/docs")
⚠️ 注意:如果返回类型不是 dict 或 Pydantic 模型,而你又忘了指定 response_class,FastAPI 会尝试把它序列化为 JSON,可能导致错误。用对 response_class 可以避免这类问题。
五、规范的exception-handling
内置 HTTP 异常抛出
最常用的exception-handling就是 HTTPException,它能在任何地方终止当前请求并返回一个清晰的错误:
from fastapi import HTTPException
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id not in items_db:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="商品不存在",
headers={"X-Error-Code": "ITEM_NOT_FOUND"}
)
return items_db[item_id]
detail 可以是字符串,也可以是字典 / 列表,方便定义业务错误码和额外信息。
业务级自定义异常
像「库存不足」这类业务逻辑错误,用 HTTPException 虽然也可以,但定义一个专属异常类,再通过 @app.exception_handler() 注册处理函数,会让代码更有表达力:
from fastapi import Request
class OutOfStockException(Exception):
def __init__(self, item_id: int, stock: int):
self.item_id = item_id
self.stock = stock
@app.exception_handler(OutOfStockException)
async def out_of_stock_handler(request: Request, exc: OutOfStockException):
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={
"code": "OUT_OF_STOCK",
"message": f"商品 {exc.item_id} 库存不足,当前剩余 {exc.stock} 件"
}
)
@app.post("/items/{item_id}/buy")
async def buy_item(item_id: int):
if items_db[item_id]["stock"] < 1:
raise OutOfStockException(item_id, 0)
items_db[item_id]["stock"] -= 1
return {"message": "购买成功"}
全局兜底异常拦截
生产环境最怕把内部异常(数据库连接失败、第三方 API 超时)直接暴露给前端。我们可以分别拦截 Pydantic 校验异常和未知的 Exception,统一格式输出:
from fastapi.exceptions import RequestValidationError
# 定制校验失败的响应格式
@app.exception_handler(RequestValidationError)
async def validation_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content={"code": "VALIDATION_ERROR", "details": exc.errors()}
)
# 兜底所有未捕获的异常
@app.exception_handler(Exception)
async def global_handler(request: Request, exc: Exception):
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={"code": "INTERNAL_ERROR", "message": "服务器开小差了,请稍后重试"}
)
🔒 安全提示:全局拦截里不要返回 exc 的具体内容(如堆栈信息),避免泄露敏感细节。
六、多状态码与自定义响应模型
一个接口可能会返回多种状态码,并且每种状态码对应的响应体结构也不一样(比如 200 返回 data,404 返回 error 信息)。这时可以用 responses 参数来在 Swagger 文档中精确描述这些分支。
from pydantic import BaseModel
class SuccessResponse(BaseModel):
code: str = "SUCCESS"
data: dict | None = None
class ErrorResponse(BaseModel):
code: str
message: str
@app.get(
"/items/{item_id}",
responses={
200: {"model": SuccessResponse, "description": "查询成功"},
404: {"model": ErrorResponse, "description": "商品不存在"}
}
)
async def read_item(item_id: int):
if item_id not in items_db:
raise HTTPException(
status_code=404,
detail={"code": "ITEM_NOT_FOUND", "message": "商品不存在"}
)
return {"code": "SUCCESS", "data": items_db[item_id]}
这样一来,在自动生成的文档里,前端可以看到每种状态码的响应示例,调用 API 时心里更有底。
七、通用分页响应方案
列表接口几乎都要做分页。FastAPI + Pydantic 的泛型支持,可以让我们写出一个「一次定义,处处使用」的通用分页模型。
from typing import Generic, List, TypeVar
T = TypeVar("T")
class PaginatedResponse(BaseModel, Generic[T]):
items: List[T] # 当前页的数据列表
total: int # 总记录数
page: int # 当前页码
page_size: int # 每页条数
total_pages: int # 总页数
@app.get("/items/", response_model=PaginatedResponse[Item])
async def list_items(page: int = 1, page_size: int = 10):
# 防止大数据量拖垮服务
page_size = min(page_size, 100)
all_items = list(items_db.values())
start = (page - 1) * page_size
end = start + page_size
total_pages = (len(all_items) + page_size - 1) // page_size
return {
"items": all_items[start:end],
"total": len(all_items),
"page": page,
"page_size": page_size,
"total_pages": total_pages
}
泛型 PaginatedResponse[Item] 告诉 FastAPI 每一页的数据是 Item 类型的列表,文档里会正确生成对应的示例,同时类型检查也更加严格。
🧠 最佳实践:永远给 page_size 设置一个上限(比如 100),避免恶意或意外的大请求导致数据库压力暴增。
八、小结
我们从最基础的响应模型开始,一路深入到exception-handling和分页,基本上覆盖了构建一个专业 API 所需的全部响应知识。最后这张速查表帮你快速回顾:
掌握这些技巧后,你的 FastAPI 应用就能输出规范、安全、又易于维护的 API 了。下一步可以继续学习依赖注入与中间件,让架构更进一步 🚀。