fastapi: 中间件记录请求参数和响应内容,并接受参数

一,相关的说明:

1,当使用 app.add_middleware() 注册中间件时,
如果你想向中间件传递自定义参数,直接将参数作为关键字参数(Keyword Arguments)传给 add_middleware 即可。

FastAPI 底层基于 Starlette,add_middleware 会自动把这些额外的参数传递给中间件类的 __init__ 构造函数。

2, 要注意的是:中间件参数是在“启动时”确定,而不是“请求时”
传给 add_middleware 的参数是单例配置。也就是说,这些参数只会在 FastAPI 项目启动(初始化中间件)时被赋值一次。

正确用法:传递数据库连接池、环境配置(如 debug=True)、超时时间、密钥等固定配置。

错误用法:试图传递当前请求的用户 ID、当前请求的 Token 等动态数据。如果需要处理请求级别的动态数据,应该在 dispatch 方法内部通过 request 对象去获取。

3,请求体(Request Body)和响应体(Response Body)在数据流中只能被读取一次。
如果不做特殊处理,中间件读取完数据后,后续的路由或前端就会拿不到数据(报错或悬挂)。

 

 

二,代码:

middleware:

# app/middleware/log_time.py
import json

from fastapi import FastAPI, Request
import time

from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import StreamingResponse

# from app.core import logger
from app.core.logger import logger

# 1. 定义你的自定义中间件
# x_header_name: str,
class LogRequestResponseMiddleware(BaseHTTPMiddleware):
    def __init__(self, app, enable_timer: bool = True):
        # 必须先调用父类的 __init__,把 app 传进去
        super().__init__(app)
        # 接收并保存你传递的自定义参数
        # self.x_header_name = x_header_name
        self.enable_timer = enable_timer

    async def dispatch(self, request: Request, call_next):

        process_time = 0

        if self.enable_timer:
            start_time = time.time()

        # ==================== 1. 捕获请求信息 ====================
        path = request.url.path
        method = request.method

        # 获取 GET 参数 (Query Params)
        query_params = dict(request.query_params)

        # 安全地获取 POST/PUT 等请求体 (Body)
        body_json = None
        if method in ["POST", "PUT", "PATCH"]:
            # receive() 会消耗数据流,所以必须通过缓存重新构建 request 保证后续路由可读
            request_body = await request.body()
            if request_body:
                try:
                    body_json = json.loads(request_body.decode("utf-8"))
                except Exception:
                    # 如果不是 JSON 格式(比如文件或表单),转为字符串记录
                    body_json = request_body.decode("utf-8", errors="ignore")

            # 关键步骤:重写请求体数据流,防止后续路由拿不到 Body
            async def receive():
                return {"type": "http.request", "body": request_body, "more_body": False}

            request._receive = receive

        # 正常处理请求
        response = await call_next(request)

        response_body = b""

        # 优先判断是否存在 body_iterator(这是流式响应的核心特征)
        if hasattr(response, "body_iterator"):
            # 逐块读取响应体内容
            res_body_chunks = [chunk async for chunk in response.body_iterator]
            response_body = b"".join(res_body_chunks)

            # 关键步骤:重新填充响应迭代器,否则前端收到的会是空数据
            async def stream_iterator():
                for chunk in res_body_chunks:
                    yield chunk

            response.body_iterator = stream_iterator()

        # 如果没有 body_iterator,但有 body 属性(普通响应)
        elif hasattr(response, "body"):
            response_body = response.body
        # 极少数特殊响应(例如某些自定义的 Response 类),如果两边都拿不到,尝试通过异步迭代器兜底
        else:
            try:
                response_body = getattr(response, "content", b"")
            except Exception:
                response_body = b"[Unreadable Response Body]"

        # 尝试解析响应体为 JSON 格式以方便阅读日志
        try:
            res_json = json.loads(response_body.decode("utf-8"))
        except Exception:
            res_json = response_body.decode("utf-8", errors="ignore")

        # 使用传递进来的参数逻辑
        if self.enable_timer:
            process_time = time.time() - start_time

        # 记录请求和响应
        # ==================== 4. 统一写入日志 ====================
        log_data = {
            "method": method,
            "path": path,
            "query_params": query_params,
            "request_body": body_json,
            "status_code": response.status_code,
            "response_body": res_json
        }

        if process_time>0:
            log_data["process_time"] = process_time


        logger.bind(log_type="alipay").info(f"API Log: {json.dumps(log_data, ensure_ascii=False)}")

        return response

 

注册中间件:

# 创建FastAPI应用,并传入 lifespan
api_app = FastAPI(title="我的API项目",
                  # docs_url=None,
                  docs_url=None,
                  redoc_url=None,
                  openapi_url=None)

# 注册全局异常捕获
register_exception_handlers(api_app)

# 注册中间件
api_app.add_middleware(LogRequestResponseMiddleware,enable_timer=True)

 

三,测试效果:

查看日志:

2026-07-25 23:10:18 | API Log: {"method": "GET", "path": "/api/users/all2", "query_params": {"ctype": "0"}, "request_body": null, 
                                "status_code": 200, 
                                "response_body": {"code": 200, "message": "success", 
                                                  "data": {"total": 6, "items": [
                                                      {"id": 1, "username": "13866669999", "nickname": "cccbbb老刘他人不错", "avatar_id": 12, "head_url": "http://127.0.0.1:8008/20260721/1540/7da829d0-a760-4696-968a-f8d506ee65b7.png", "avatar_url": "https://cdn.example.com/avatars/1.png"}, 
                                                      {"id": 2, "username": "123", "nickname": "测试1", "avatar_id": 0, "head_url": "http://127.0.0.1:8008/logo_300.png", "avatar_url": "https://cdn.example.com/avatars/2.png"}, 
                                                      {"id": 3, "username": "321", "nickname": "测试2", "avatar_id": 0, "head_url": "http://127.0.0.1:8008/logo_300.png", "avatar_url": "https://cdn.example.com/avatars/3.png"}]}}, 
                                "process_time": 0.022850751876831055}

 

posted @ 2026-07-25 23:19  刘宏缔的架构森林  阅读(16)  评论(0)    收藏  举报