FastAPI 中中间件和异常处理机制是怎样的?
简化版
FastAPI 的中间件用于在请求进入路由前后统一处理逻辑,比如日志、耗时统计、CORS、Trace ID。异常处理用于把业务异常、校验异常或系统异常转换成统一响应,常见方式是抛 HTTPException 或注册自定义 exception handler。
详细版
中间件关注请求链路的横切逻辑:
- 请求日志;
- 响应耗时;
- CORS;
- Trace ID;
- 统一响应头;
- 简单鉴权拦截。
异常处理关注错误如何返回给客户端:
HTTPException用于主动返回业务错误状态码;RequestValidationError用于请求参数校验失败;- 自定义异常可以通过
@app.exception_handler注册处理器; - 不应把所有异常都吞成 200 响应。
示例:
@app.middleware("http")
async def log_time(request, call_next):
response = await call_next(request)
return response
@app.exception_handler(BusinessError)
async def handle_business_error(request, exc):
return JSONResponse(status_code=400, content={"message": exc.message})
完整版教学
一、中间件解决的是什么问题
中间件处理的是横切关注点。所谓横切关注点,就是很多接口都需要,但又不属于某个具体业务的逻辑。例如每个请求都要记录日志,每个响应都要带 Trace ID,每个接口都要统计耗时。
如果这些逻辑写在每个路由函数里,代码会重复,而且容易不一致。中间件可以把这类逻辑放在请求链路的统一入口和出口。
FastAPI 继承了 Starlette 的中间件机制。一个 HTTP 中间件大致结构是:
@app.middleware("http")
async def middleware(request: Request, call_next):
# 请求进入路由前
response = await call_next(request)
# 路由处理完成后
return response
call_next(request) 表示把请求继续交给后续中间件或真正的路由处理函数。
二、中间件的典型用法
请求耗时统计是最常见例子:
import time
from fastapi import Request
@app.middleware("http")
async def add_process_time(request: Request, call_next):
start = time.perf_counter()
response = await call_next(request)
duration = time.perf_counter() - start
response.headers["X-Process-Time"] = str(duration)
return response
Trace ID 也适合放在中间件中。服务收到请求时读取或生成一个 trace id,放入日志上下文和响应头,方便排查链路问题。
CORS 也属于中间件。FastAPI 通常使用 CORSMiddleware,配置允许的源、方法、请求头等。它不是业务逻辑,而是浏览器跨域访问控制策略。
三、异常处理解决的是什么问题
异常处理的目标是把不同类型的错误转换成稳定、可理解的 API 响应。没有统一异常处理时,系统错误可能直接暴露堆栈,业务错误可能格式各异,前端难以处理。
FastAPI 提供了 HTTPException:
from fastapi import HTTPException
@app.get("/users/{user_id}")
def get_user(user_id: int):
user = find_user(user_id)
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
return user
它适合在业务逻辑中主动表达 HTTP 错误状态,比如 404、403、409。
四、自定义异常处理器
大型项目通常会定义业务异常:
class BusinessError(Exception):
def __init__(self, code: str, message: str):
self.code = code
self.message = message
然后注册处理器:
from fastapi.responses import JSONResponse
@app.exception_handler(BusinessError)
async def business_error_handler(request: Request, exc: BusinessError):
return JSONResponse(
status_code=400,
content={"code": exc.code, "message": exc.message},
)
这样业务层可以抛出统一异常,API 层统一转换响应。错误格式会更稳定,前端也更容易根据 code 做提示。
五、中间件和依赖注入的边界
很多人会把中间件、依赖注入和异常处理混在一起。它们确实都在请求链路里,但职责不同。
中间件适合非常通用、几乎所有请求都需要的逻辑,比如日志、耗时、CORS、Trace ID。依赖注入适合某些接口需要的逻辑,比如当前用户、权限、数据库 session。异常处理适合把错误转换成统一响应。
不要把复杂权限系统全部写在中间件里,因为不同接口的权限规则可能不同。更常见做法是:中间件做 trace、日志、跨域;依赖注入做当前用户和权限;异常处理做错误响应。
六、异常处理的常见错误
第一个错误是把所有异常都转成 HTTP 200。这会破坏 HTTP 语义,让调用方无法根据状态码判断成功失败,也会影响监控和告警。
第二个错误是把内部异常信息直接返回给用户。数据库错误、堆栈信息、SQL 语句都可能泄露系统内部细节。
第三个错误是中间件捕获异常后不重新抛出,也不返回合理响应,导致请求挂起或错误被静默吞掉。
第四个错误是异常格式不统一。一个接口返回 message,另一个接口返回 error_msg,前端和客户端都会痛苦。
七、常见误区与追问
| 机制 | 主要职责 | 典型例子 |
|---|---|---|
| 中间件 | 请求前后统一处理横切逻辑 | Trace ID、耗时、CORS、访问日志 |
| 依赖注入 | 某些接口需要的前置资源和校验 | 当前用户、权限、DB session |
| 异常处理器 | 把异常转换成稳定响应 | BusinessError -> JSON 错误体 |
请求 -> 中间件前置 -> 依赖解析 -> 路由函数
<- 中间件后置 <- 异常处理器把错误转响应
记忆钩子:中间件管“所有请求都要经过的路”,异常处理管“出错以后怎么对外说”。
- 误区:把所有业务鉴权都塞进中间件。 中间件适合通用横切逻辑,复杂权限经常依赖具体路由和资源,放在 Depends 或业务层更清楚。
- 误区:所有错误都包装成 HTTP 200。 这样会破坏 HTTP 语义,监控、重试、客户端错误处理都会失真。
- 误区:把内部异常堆栈直接返回给客户端。 SQL、堆栈、内部服务地址可能泄露系统细节,生产环境应记录日志但返回稳定错误格式。
- 追问:
HTTPException和自定义异常处理器怎么配合? 主动表达 HTTP 错误可直接抛HTTPException;业务域错误可以定义异常类,再用@app.exception_handler统一转响应。 - 追问:中间件里
call_next的作用是什么? 它把请求交给后续中间件和路由处理;不调用或不返回响应,请求链路就不会正常继续。 - 追问:为什么耗时统计适合中间件? 它需要覆盖大量接口,并且天然包住请求前后;用中间件可以统一记录开始时间、结束时间、状态码和 trace id。
八、加强记忆
记 FastAPI 请求链路可以这样分工:中间件管所有请求的横切逻辑,依赖注入管某些接口需要的前置资源和权限,异常处理管错误如何变成稳定响应。不要用 200 包装所有失败,也不要把内部异常细节暴露给客户端。