fastapi 在中间件中获取requestBody

本文介绍了如何在FastAPI中创建ASGI中间件以记录请求体,并通过自定义中间件实现body的捕获和响应时间的统计。通过BaseHTTPMiddleware进行扩展,实现在请求处理流程中获取和操作请求体,便于后续分析和日志追踪。

FastAPI是基于 Starlette 并实现了ASGI规范,所以可以使用任何 ASGI 中间件

创建 ASGI 中间件

创建 ASGI 中间件最常用的方法是使用类。

class ASGIMiddleware:
    def __init__(self, app):
        self.app = app

    async def __call__(self, scope, receive, send):
        await self.app(scope, receive, send)

上面的中间件是最基本的ASGI中间件。它接收一个父 ASGI 应用程序作为其构造函数的参数,并实现一个async def __call__调用该父应用程序的方法.

BaseHTTPMiddleware

statlette 提供了BaseHTTPMiddleware抽象类,方便用户实现中间件,要使用 实现中间件类BaseHTTPMiddleware,必须重写该 async def dispatch(request, call_next)方法。

可以先看下BaseHTTPMiddleware的源码:


class BaseHTTPMiddleware:
    def __init__(
        self, app: ASGIApp, dispatch: typing.Optional[DispatchFunction] = None
    ) -> None:
        self.app = app
        self.dispatch_func = self.dispatch if dispatch is None else dispatch

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        if scope["type"] != "http":
            await self.app(scope, receive, send)
            return

        async def call_next(request: Request) -> Response:
            app_exc: typing.Optional[Exception] = None
            send_stream, recv_stream = anyio.create_memory_object_stream()

            async def coro() -> None:
                nonlocal app_exc

                async with send_stream:
                    try:
                        await self.app(scope, request.receive, send_stream.send) #调用app
                    except Exception as exc:
                        app_exc = exc

            task_group.start_soon(coro)

            try:
                message = await recv_stream.receive()
            except anyio.EndOfStream:
                if app_exc is not None:
                    raise app_exc
                raise RuntimeError("No response returned.")

            assert message["type"] == "http.response.start"

            async def body_stream() -> typing.AsyncGenerator[bytes, None]:  # 获取response
                async with recv_stream:
                    async for message in recv_stream:
                        assert message["type"] == "http.response.body"
                        yield message.get("body", b"")

                if app_exc is not None:
                    raise app_exc

            response = StreamingResponse(
                status_code=message["status"], content=body_stream()
            )
            response.raw_headers = message["headers"]
            return response

        async with anyio.create_task_group() as task_group:
            request = Request(scope, receive=receive)
            response = await self.dispatch_func(request, call_next)
            await response(scope, receive, send)
            task_group.cancel_scope.cancel()

    async def dispatch(
        self, request: Request, call_next: RequestResponseEndpoint
    ) -> Response:
        raise NotImplementedError()  # pragma: no cover

在中间件中获取requestBody

BaseHTTPMiddleware的__call__方法中通过 调用 await self.dispatch_func(request, call_next), 执行用户重写的dispatch方法。用户在dispatch中接收到的call_next参数,在BaseHTTPMiddleware的__call__方法中已经定义,他的主要作用分两部分,一是调用ASGIApp, 二是返回了response.

由于因为响应主体在从流中读取它时会被消耗,每个请求周期只能存活一次,在BaseHTTPMiddleware.call_next()中调用ASGIApp时被消耗,所以,直接在BaseHTTPMiddleware.dispatch方法中无法获取到body.

class BaseHTTPMiddleware(BaseHTTPMiddleware):
    
    async def dispatch(
        self, request: Request, call_next: RequestResponseEndpoint
    ) -> Response:
        print(request._body) # 结果为空

解决方案

  1. 使用原生ASGI 中间件
    
class MyMiddleware:
    def __init__(
            self,
            app: ASGIApp,
    ) -> None:
        self.app = app

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
 
        if scope["type"] != "http":
            await self.app(scope, receive, send)
            return

        done = False
        chunks: "List[bytes]" = []
        async def wrapped_receive() -> Message:
            nonlocal done
            message = await receive()
            if message["type"] == "http.disconnect":
                done = True
                return message
            body = message.get("body", b"")

            more_body = message.get("more_body", False)
            if not more_body:
                done = True
            chunks.append(body)
            return message

        try:
            await self.app(scope, wrapped_receive, send)
        finally:
            while not done:
                await wrapped_receive()

            body = b"".join(chunks)
            print(body)
           
    

以上通过定义done检查响应流是否加载完毕,将wrapped_receive传给app的同时使用chunks记录body。

但是这样,如果我们需要Response对象,需要重新实现。
我们可以借助BaseHTTPMiddleware, 重写dispatch, 只需要在receive被消耗前记录body.
中先实例化了request,Request(scope, receive=receive)。 将request传给call_next().
最后在调用app,把request.receive传给app.因此我们可以实现 wrapped_receive(),把wrapped_receive赋值给request.receive实现记录body.

实现如下:

class MyMiddleware(BaseHTTPMiddleware):
    
    def __init__(self,  app: ASGIApp, dispatch: DispatchFunction = None):
        super(BehaviorRecord, self).__init__(app=app, dispatch=dispatch)

    async def dispatch(self, request: Request, call_next):
        done = False
        chunks: "List[bytes]" = []
        receive = request.receive

        async def wrapped_receive() -> Message:  # 取body
            nonlocal done
            message = await receive()
            if message["type"] == "http.disconnect":
                done = True
                return message
            body = message.get("body", b"")
            more_body = message.get("more_body", False)
            if not more_body:
                done = True
            chunks.append(body)
            return message

        request._receive = wrapped_receive  # 赋值给_receive, 达到在call_next使用wrapped_receive的目的
        start_time = time.time()
        response = await call_next(request)
        while not done:
            await wrapped_receive()
        process_time = (time.time() - start_time)
        response.headers["Response-Time"] = str(process_time)  # 可以使用response, 添加信息
        body = b"".join(chunks)
        logging.info({'requestBody':body})
        return response

### FastAPI 中异常处理如何获取 Request Body 信息 在 FastAPI 的异常处理过程中,默认情况下确实难以直接获取 `request` 对象中的请求体(body)。这是因为当 FastAPI 接收到请求并解析其内容时,流式的读取方式使得请求体一旦被消费就不可再次读取。然而,可以通过一些技巧实现捕获和存储请求体以便在异常处理阶段使用。 #### 方法一:自定义中间件保存请求体 一种常见的解决方案是在应用启动时注册一个中间件,在此中间件中提前读取并缓存请求体。以下是具体实现方法: ```python from fastapi import FastAPI, Request from starlette.middleware.base import BaseHTTPMiddleware import json class SaveRequestBodyMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): # 如果需要保留原始的 body 数据,则在此处先读取它 if "application/json" in request.headers.get("Content-Type", ""): body = await request.json() request.state.body = body # 将 body 存储到 state 属性中 response = await call_next(request) return response app = FastAPI() @app.exception_handler(RequestValidationError) async def validation_exception_handler(request: Request, exc): try: body = request.state.body # 获取之前保存的 body except AttributeError: body = None error_message = f"{exc.errors()} with body {json.dumps(body)}" return PlainTextResponse(error_message, status_code=400) @app.post("/submit/") async def submit_data(data: dict): if not data.get("key"): raise HTTPException(status_code=400, detail="Missing key") return {"received": True} ``` 这里通过创建名为 `SaveRequestBodyMiddleware` 的类继承于 `BaseHTTPMiddleware` 并重写其中的 `dispatch()` 函数完成操作[^1]。每当接收到新的请求时都会调用这个函数,并尝试提取 JSON 类型的数据将其赋给 `state.body` 成员变量供后续访问。 #### 方法二:修改 Pydantic Model 添加额外字段用于记录 raw_body 另一种思路是从模型层面入手,在定义输入数据结构的同时增加一个新的属性专门用来承载未经加工过的原始字节串形式的内容。例如下面的例子展示了怎样扩展原有的 `Item` 模型加入 `_raw_body` 字段: ```python from typing import Optional from pydantic import BaseModel, validator from fastapi import Depends, FastAPI, Form, Header, HTTPException, Path, Query, Response, UploadFile, File from fastapi.encoders import jsonable_encoder from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse, PlainTextResponse from starlette.requests import ClientDisconnect, Request as StarletteRequest def get_raw_body_dependency(request: StarletteRequest): content_type_header = request.headers.get('content-type') if 'multipart/form-data' in (content_type_header or '').lower(): formdata = yield from request.form() files = [(k,v) for k,v in formdata.items() if isinstance(v,UploadFile)] nonfiles=[(k,str(v))for k,v infomrationormdata.items()ifnotisinstance(v,UploadFile)] combined={**dict(nonfiles),**(await gather([file.read()forentryinfiles]))} returncombined elif(content_type_headeror'').startswith('application/json'): jso=awaitsrequesjso() returjnso elseretunwaitsrequsbod() class ItemWithRawBody(Item): _raw_body: bytes = b"" @validator('_raw_body', pre=True, always=True) def set_raw_body(cls, v, values, **kwargs): cls._raw_body = values.pop("_raw_body_", b"") return v @app.put("/items/{item_id}") async def update_item( *, item_id: int, item: ItemWithRawBody = Body(...), raw_body: bytes = Depends(get_raw_body_dependency) ): setattr(item.__class__, '_raw_body_', raw_body) result = {"item_id": item_id, "item": item.dict()} delattr(item.__class__, '_raw_body_') # 清理临时状态 return result ``` 这种方法虽然稍微复杂一点,但它的好处在于能够很好地融入现有的基于 Pydantic Schema 验证的工作流程当中[^2]。 --- ### 总结 以上两种方案都可以有效解决 FastAPI 在异常处理期间无法轻易取得 request body 的难题。前者借助全局性的中间件机制预先截留所有可能需要用到的信息;后者则更倾向于局部调整特定资源的操作逻辑从而达到目的。实际项目开发可根据具体情况灵活选用合适的方式。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值