FastAPI中间件

一、中间件核心概念

在 FastAPI 中,中间件是介于「客户端请求」和「应用程序核心路由逻辑」之间的轻量级软件层,本质是一个异步函数,能够拦截所有请求和响应,实现统一的全局处理逻辑。

中间件的核心作用

中间件主要用于处理所有请求/响应的公共逻辑,无需在每个路由中重复编写,常见用途包括:

  • 拦截客户端传入的请求,进行前置处理(如验证请求头、记录请求日志、解析Token);
  • 调用应用程序核心路由逻辑,获取响应结果;
  • 拦截路由返回的响应,进行后置处理(如添加响应头、统一响应格式、记录响应耗时);
  • 全局异常捕获、跨域设置、请求限流等全局统一操作。

中间件的执行流程

  1. 客户端发送请求到 FastAPI 应用;
  2. 请求先经过所有注册的中间件(按注册顺序执行前置处理);
  3. 中间件通过call_next(request) 调用下一个中间件,最终调用对应的路由处理函数;
  4. 路由处理函数返回响应,响应再按「反向顺序」经过所有中间件(执行后置处理);
  5. 最终处理后的响应返回给客户端。

二、中间件基本结构

FastAPI 提供 @app.middleware("http") 装饰器来定义中间件,中间件函数必须遵循固定的参数和返回值规范,以下是最基础的中间件模板,可直接复用:

from fastapi import FastAPI, Request, Response
import time

# 1. 创建 FastAPI 应用实例
app = FastAPI()

# 2. 定义中间件:使用 @app.middleware("http") 装饰器
@app.middleware("http")
async def custom_middleware(request: Request, call_next):
    """
    自定义基础中间件模板
    Args:
        request: 请求对象,包含客户端请求的所有信息(方法、URL、请求头、请求体等)
        call_next: 可调用对象,用于调用下一个中间件或路由处理函数,必须传入 request
    Returns:
        Response: 处理后的响应对象,必须返回 Response 或其子类(如 JSONResponse)
    """
    # ------------------- 1. 请求前处理(前置逻辑)-------------------
    # 示例:记录请求开始时间(用于计算接口耗时)
    start_time = time.time()
    # 可添加其他前置操作:验证请求头、打印请求信息、解析Token等
    
    # ------------------- 2. 调用后续逻辑(核心)-------------------
    # 调用下一个中间件或路由函数,获取响应对象
    # 注意:必须使用 await 调用 call_next,且传入 request 参数
    response = await call_next(request)
    
    # ------------------- 3. 响应后处理(后置逻辑)-------------------
    # 示例:计算接口耗时,添加到响应头中
    process_time = time.time() - start_time
    # 给响应添加自定义响应头(X-开头为自定义头,避免与标准头冲突)
    response.headers["X-Process-Time"] = str(process_time)
    # 可添加其他后置操作:记录响应日志、统一响应格式、处理异常等
    
    # ------------------- 4. 返回响应-------------------
    return response

# 测试路由:用于验证中间件是否生效
@app.get("/test")
async def test_route():
    return {"message": "测试中间件生效"}

三、中间件关键组件详解

中间件的核心是 requestcall_nextresponse 三个组件,掌握其用法是编写中间件的关键,以下是详细说明:

3.1 request: Request(请求对象)

由 FastAPI 自动传入,包含客户端请求的所有信息,常用属性和方法:

  • request.method:请求方法(GET、POST、PUT、DELETE 等);
  • request.url:请求完整 URL(如 http://127.0.0.1:8000/test);
  • request.headers:请求头(字典格式,可获取 Token、Content-Type 等);
  • request.query_params:查询参数(如 /test?id=123 中的 id);
  • await request.body():获取请求体(异步方法,需用 await 调用);
  • request.client:客户端信息(IP 和端口,如 ('127.0.0.1', 54321))。

3.2 call_next(调用链函数)

一个可调用的异步函数,核心作用是「传递请求到下一个环节」,必须满足两个要求:

  • 调用时必须传入 request 对象(不可省略,否则后续环节无法获取请求信息);
  • 必须使用 await 调用(因为 call_next 是异步函数);
  • 返回值是 Response 对象(即路由或下一个中间件返回的响应)。

注意:如果在中间件中不调用 call_next(request),请求将被拦截,无法到达路由处理函数,客户端会一直等待响应(超时)。

3.3 response: Response(响应对象)

call_next(request) 返回,包含路由处理后的响应信息,常用操作:

  • response.status_code:响应状态码(200、404、500 等);
  • response.headers:响应头(可添加、修改、删除响应头信息);
  • response.body:响应体(bytes 格式,可修改响应内容)。

四、实战案例:常用中间件实现

以下是两个最常用的中间件实战案例,可直接复制到项目中使用,覆盖「请求日志记录」和「全局响应头设置」两个高频场景。

案例1:请求日志记录中间件

功能:记录所有请求的「请求方法、请求URL、客户端IP」,以及响应的「状态码、处理耗时」,便于调试和问题排查(结合 logging 模块)。

from fastapi import FastAPI, Request
import time
import logging

# 1. 配置日志(格式化输出,便于查看)
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)

# 2. 创建 FastAPI 应用
app = FastAPI()

# 3. 定义日志记录中间件
@app.middleware("http")
async def log_requests(request: Request, call_next):
    """
    全局请求日志记录中间件
    记录请求信息、响应状态码、处理耗时
    """
    # 前置处理:记录请求信息
    client_ip = request.client.host  # 客户端IP
    method = request.method          # 请求方法
    url = str(request.url)           # 请求URL
    logger.info(f"收到请求 | IP: {client_ip}, 方法: {method}, URL: {url}")
    
    # 记录请求开始时间(用于计算耗时)
    start_time = time.time()
    
    # 调用下一个中间件/路由
    response = await call_next(request)
    
    # 后置处理:记录响应信息和耗时
    process_time = round(time.time() - start_time, 4)  # 保留4位小数
    status_code = response.status_code                 # 响应状态码
    logger.info(f"返回响应 | 状态码: {status_code}, 耗时: {process_time}s")
    
    return response

# 测试路由
@app.get("/user/{user_id}")
async def get_user(user_id: int):
    return {"user_id": user_id, "message": "查询用户成功"}

@app.post("/user")
async def create_user():
    return {"message": "创建用户成功"}, 201

案例2:全局响应头中间件

功能:给所有响应统一添加自定义响应头,如接口版本、服务器标识、跨域相关头,无需在每个路由中单独设置。

from fastapi import FastAPI, Request, Response

app = FastAPI()

# 定义全局响应头中间件
@app.middleware("http")
async def add_global_headers(request: Request, call_next):
    """
    全局响应头中间件
    给所有响应添加统一的自定义响应头
    """
    # 调用下一个中间件/路由,获取响应
    response = await call_next(request)
    
    # 后置处理:添加自定义响应头
    response.headers["X-API-Version"] = "v1.0.0"    # 接口版本
    response.headers["X-Server"] = "FastAPI-Server" # 服务器标识
    response.headers["Access-Control-Allow-Origin"] = "*"  # 跨域允许(开发环境)
    
    return response

# 测试路由:响应会自动带上上面的3个响应头
@app.get("/demo")
async def demo():
    return {"message": "全局响应头测试成功"}

五、多个中间件的注册与执行顺序

FastAPI 支持注册多个中间件,执行顺序遵循「注册顺序=前置处理顺序,反向顺序=后置处理顺序」,以下是示例说明:

from fastapi import FastAPI, Request

app = FastAPI()

# 中间件1:第一个注册,最先执行前置处理,最后执行后置处理
@app.middleware("http")
async def middleware1(request: Request, call_next):
    print("中间件1:前置处理")
    response = await call_next(request)
    print("中间件1:后置处理")
    return response

# 中间件2:第二个注册,第二个执行前置处理,第二个执行后置处理
@app.middleware("http")
async def middleware2(request: Request, call_next):
    print("中间件2:前置处理")
    response = await call_next(request)
    print("中间件2:后置处理")
    return response

# 测试路由
@app.get("/test")
async def test():
    print("路由函数执行")
    return {"message": "测试多个中间件"}

执行顺序输出

中间件1:前置处理
中间件2:前置处理
路由函数执行
中间件2:后置处理
中间件1:后置处理

六、关键注意事项

  • 异步要求:中间件函数必须是异步函数(async def),call_next(request) 必须用 await 调用,否则会报错。
  • 返回值要求:中间件必须返回 Response 对象或其子类(如 JSONResponseHTMLResponse),不能返回普通字典或字符串。
  • 请求体读取:如果在中间件中读取了请求体(await request.body()),后续路由中再读取请求体会为空,需使用 request.body() 的缓存机制(如 body = await request.body(),再通过Request(scope=request.scope, receive=lambda: {"type": "http.request", "body": body}) 重新构造请求)。
  • 开发与生产环境区分:跨域相关头(如 Access-Control-Allow-Origin: *)仅适合开发环境,生产环境需指定具体的客户端域名,避免安全风险。
  • 中间件粒度:中间件用于全局公共逻辑,单个中间件职责尽量单一(如只做日志记录、只加响应头),避免一个中间件包含过多逻辑,便于维护。

七、常见问题排查

  • 问题1:中间件不生效:检查是否使用 @app.middleware("http") 装饰器,是否在路由注册前定义中间件(FastAPI 会按代码顺序注册中间件,路由需在中间件之后定义)。
  • 问题2:调用 call_next 时报错:确认 call_next 传入的是 request 对象,且中间件是异步函数、call_next 用 await 调用。
  • 问题3:路由无法读取请求体:检查中间件是否读取了请求体且未重新构造请求,需缓存请求体后重新传入后续环节。
  • 问题4:响应头添加失败:确认 response 是 Response 对象,而非普通字典;部分响应头(如 Content-Length)是自动生成的,手动修改可能不生效。
posted @ 2026-02-04 17:07  向闲而过  阅读(0)  评论(0)    收藏  举报