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 模型把输出数据「清洗」一遍,只留下 idnameemail。对前端来说,密码从未暴露。

模型的核心价值

别以为响应模型只是「过滤字段」那么简单,它还能带来三大收益:

  1. 数据转换与校验:数据库返回的 Decimal 字段可以自动转成 JSON 友好的 float;缺失的可选字段会补全默认值;字段类型不对还会直接报错,防止脏数据流出。
  2. 自动生成 Swagger / OpenAPI 文档:前端开发对着文档一看,就能了解字段类型、必填项、示例值,沟通成本直线下降。
  3. 开发契约:前后端共享 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 有两种写法:

  1. 直接写数字(小巧但不易维护)
  2. 使用 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 意味着删除成功,但响应体没有任何内容,浏览器也不会跳转页面。

高频状态码速查表

记住下面这些状态码,日常开发基本够用:

状态码常量说明(RESTful 语义)
200HTTP_200_OK查询、更新、部分更新成功
201HTTP_201_CREATED创建资源成功(比如 POST 注册)
204HTTP_204_NO_CONTENT删除成功或空查询
400HTTP_400_BAD_REQUEST请求参数格式/逻辑错误
401HTTP_401_UNAUTHORIZED未登录或 Token 无效
403HTTP_403_FORBIDDEN已登录但没权限操作该资源
404HTTP_404_NOT_FOUND资源不存在
422HTTP_422_UNPROCESSABLE_ENTITYFastAPI / Pydantic 校验失败
500HTTP_500_INTERNAL_SERVER_ERROR服务器内部不可控错误

💡 小技巧:遇到客户端传参错误,可以返回 400;数据校验失败则交给 FastAPI 自动返回 422;权限问题用 403,防止攻击者猜出资源是否存在的用 404 遮掩。


三、直接操作 Response 对象

当简单的 response_modelstatus_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 去二次处理,所以你可以自由掌控响应的一切细节。

除了直接返回 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 所需的全部响应知识。最后这张速查表帮你快速回顾:

功能场景核心用法/类/参数
声明响应契约 + 过滤字段response_model=PublicUser
灵活调整单个接口字段response_model_exclude/include/unset
统一语义的状态码fastapi.status.HTTP_201_CREATED
完全自定义 JSON 响应fastapi.responses.JSONResponse
加响应头 / 设 Cookie注入 Response 参数
切换响应类型response_class=HTMLResponse
抛出规范业务异常raise HTTPException
自定义业务异常定义类 + @app.exception_handler()
全局兜底拦截拦截 Exception
多状态码 + 多模型文档responses={200: {"model": Success}}
通用分页Pydantic 泛型 PaginatedResponse[T]

掌握这些技巧后,你的 FastAPI 应用就能输出规范、安全、又易于维护的 API 了。下一步可以继续学习依赖注入与中间件,让架构更进一步 🚀。