← 返回题目列表

FastAPI 中中间件和异常处理机制是怎样的?

高频 中等 第 14 / 27 题 更新于 2026/07/27
中间件异常处理HTTPException请求链路

简化版

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 包装所有失败,也不要把内部异常细节暴露给客户端。