ITPub博客

首页 > 数据库 > 数据库开发技术 > fastapi框架源码分析系列-(8)fastapi中间件消费body消息体问题纠正

fastapi框架源码分析系列-(8)fastapi中间件消费body消息体问题纠正

原创 数据库开发技术 作者:程序员小钟同学 时间:2022-06-12 19:46:18 0 删除 编辑

问题目标

我一直想定义一个日志类型的中间件,且再这个中间件中可以自由读取到消费我们的body,不会因为在路由里面进行消费,或其他中间件也消费的时候引发阻塞,也就是多重消费问题!通过多种方式间接和源码的理解,最终还是可以解决了!

前言


本篇主要是为了纠正上一篇提出的方案,测试之后存在问题!需要纠正!所以前面那一片建议不用看了!上一篇的地址为

fastapi框架源码分析系列-starlette框架之Debug方式学源码(5)fastapi中间件消费body消息体再次消费

上一篇中提到关于双重中间件消费请求中requeset中的body受阻的问题,有探讨过一种上方式就是,单独使用一个中间件获取到requeset中的body然后赋值到,可以完美处理此类需求:

from fastapi import FastAPI, Header
from starlette.requests import Request
from starlette.responses import Response, StreamingResponse, PlainTextResponse
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.types import Message

app = FastAPI()

from pydantic import BaseModel


class AdminLogin(BaseModel):
    """
    管理员登陆 提交的参数信息
    "
""
    password: str = None
    username: str = None


class LogerMiddleware1(BaseHTTPMiddleware):

    async def set_body(self, request):
        # 定义一个携程对象,返回是一个
        async def receive():
            return receive_
        receive_ = await request._receive()
        request._receive = receive

    async def dispatch(self, request: Request, call_next):
        # await self.proxy_get_body(request)
        re = await self.set_body(request)
        print("中间件消费", re)
        response = await call_next(request)
        return response

class LogerMiddleware2(BaseHTTPMiddleware):

    async def set_body(self, request):
        # 定义一个携程对象,返回是一个
        async def receive():
            return receive_
        receive_ = await request._receive()
        request._receive = receive

    async def dispatch(self, request: Request, call_next):
        # await self.proxy_get_body(request)
        re = await self.set_body(request)
        print("中间件消费", re)
        response = await call_next(request)
        return response

app.add_middleware(LogerMiddleware2)
app.add_middleware(LogerMiddleware1)


@app.get("/on")
async def main(res: Request, sdsada: AdminLogin):
    # 迭代的方式返回流数据
    # buas = res.state.body
    # print("路由中在消费---》", buas)
    return PlainTextResponse('fanhui')


if __name__ == '__main__':
    import uvicorn

    uvicorn.run('ceshi88:app')

上面完整代码运行起来其实会受阻,归根结底病症还是多重消费问题,上面的问题涉及到三次消费:

  • 消费1:LogerMiddleware2
  • 消费2:LogerMiddleware1
  • 消费3:sdsada: AdminLogin
  • 甚至如果再路由路由直接通过res: Request在消费一次都可能有四次的消费了!

引发思考点:

加入我坚持需要都在各自中间中消费各自的body,且相互不影响的话,那该怎么办呢?

方案1:最外层中间件读取后放置到state中,如:request.state.body=

from fastapi import FastAPI, Header
from starlette.requests import Request
from starlette.responses import Response, StreamingResponse, PlainTextResponse
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.types import Message

app = FastAPI()

from pydantic import BaseModel


class AdminLogin(BaseModel):
    """
    管理员登陆 提交的参数信息
    "
""
    password: str = None
    username: str = None


class LogerMiddleware1(BaseHTTPMiddleware):

    async def set_body(self, request):
        # 定义一个携程对象,返回是一个
        async def receive():
            return receive_

        receive_ = await request._receive()
        request._receive = receive

    async def dispatch(self, request: Request, call_next):
        re = await self.set_body(request)
        request.state.body = re
        print("中间件消费", re)
        response = await call_next(request)
        return response


class LogerMiddleware2(BaseHTTPMiddleware):

    async def dispatch(self, request: Request, call_next):
        buas = request.state.body
        print("中间件消费22", buas)
        response = await call_next(request)
        return response


# app.add_middleware(LogerMiddleware2)
app.add_middleware(LogerMiddleware1)


@app.get("/on")
async def main(res: Request, sdsada: AdminLogin):
    # 迭代的方式返回流数据
    buas = res.state.body
    # print("路由中在消费---》", buas)
    return PlainTextResponse('fanhui')

@app.get("/on2")
async def main(res: Request, sdsada: AdminLogin):
    # 迭代的方式返回流数据--路由内可以多次重复消费读取
    buas = res.state.body
    s = await res.body()
    # print("路由中在消费---》", buas)
    return PlainTextResponse('fanhui')


if __name__ == '__main__':
    import uvicorn

    uvicorn.run('ceshi8:app')

上述案例似乎是完美的,看似可以解决问题!!!!!但是但是但是!!!!问题点在于当我注册两个中间件的的时候,那么就又遇到被重复消费的问题了!!!也就是,其实第一个中间件读取了,放到了scope中保存,但是还是会遇到二次消费的问题!

app.add_middleware(LogerMiddleware2)
app.add_middleware(LogerMiddleware1)

注册两个都基于BaseHTTPMiddleware中间件的时候又遇到问题了!所以上面那种的方案!!又需要废弃了!!!因为不可能我用了一个之后 后续都不能继续注册中间件了是吧!!!

所以注意上面的方案宣告废弃!!!!

方案2:再变形!!!!(1)-完全自主独立

from fastapi import FastAPI
from starlette.responses import PlainTextResponse, RedirectResponse, Response
from starlette.types import ASGIApp, Receive, Scope, Send
from fastapi import Request, Depends

app = FastAPI()


class LogerMiddleware1:
    def __init__(
            self,
            app: ASGIApp,

    ) -> None:
        self.app = app

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

        # 定义一个新的携程函数---->
        # 接收一次
        receive_ = await receive()
        async def receive():
            return receive_
        # 创建需要解析的参数
        request = Request(scope, receive)
        re = await request.json()
        print('中间件1中多次消费!22212', re)


        await self.app(scope, receive, send)


class LogerMiddleware2:
    def __init__(
            self,
            app: ASGIApp,

    ) -> None:
        self.app = app

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

        # 接收一次
        receive_ = await receive()

        async def receive():
            return receive_

        # 创建需要解析的参数
        request = Request(scope, receive)
        re2 = await request.body()
        print('中间件2中多次消费!12', re2)
        re = await request.json()
        print('中间件2中多次消费!12', re)
        # 把接收转化为一个新的携程对象穿
        await self.app(scope, receive, send)


app.add_middleware(LogerMiddleware1)
app.add_middleware(LogerMiddleware2)

from pydantic import BaseModel


class AdminLogin(BaseModel):
    """
    管理员登陆 提交的参数信息
    "
""
    password: str = None
    username: str = None


@app.get("/on")
async def main(res: Request, sdsada: AdminLogin):
    # 迭代的方式返回流数据
    buas = await res.json()
    print("路由中单独再消费", buas)
    return PlainTextResponse('fanhui')


if __name__ == "__main__":
    import uvicorn
    import os

    app_modeel_name = os.path.basename(__file__).replace(".py", "")
    print(app_modeel_name)
    uvicorn.run(f"{app_modeel_name}:app", host='127.0.0.1', reload=True)

通过上面这种方式,则可以完美解决,我每个中间件中都可以独立提取body进行消费了!!也不需要以前那种必须在自定义一路由类中进行路由日志记录了!!!

方案2:再变形!!!!(2)-最外层中间读取后写入request.state.body

完整代码示例:

from fastapi import FastAPI
from starlette.responses import PlainTextResponse, RedirectResponse, Response
from starlette.types import ASGIApp, Receive, Scope, Send
from fastapi import Request, Depends

app = FastAPI()


class LogerMiddleware1:
    def __init__(
            self,
            app: ASGIApp,

    ) -> None:
        self.app = app

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

        # 定义一个新的携程函数---->
        # 接收一次
        # receive_ = await receive()
        # async def receive():
        #     return receive_
        # 创建需要解析的参数
        request = Request(scope, receive)
        # re = await request.json()
        re = request.state.body
        print('中间件1中多次消费!22212', re)

        await self.app(scope, receive, send)


class LogerMiddleware2:
    def __init__(
            self,
            app: ASGIApp,

    ) -> None:
        self.app = app

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

        # 接收一次
        receive_ = await receive()

        async def receive():
            return receive_

        # 创建需要解析的参数
        request = Request(scope, receive)
        re2 = await request.body()
        print('中间件2中多次消费!12', re2)
        re = await request.json()
        print('中间件2中多次消费!12', re)
        # buas = request.state.body
        request.state.body = re
        # 把接收转化为一个新的携程对象穿
        await self.app(scope, receive, send)


app.add_middleware(LogerMiddleware1)
app.add_middleware(LogerMiddleware2)

from pydantic import BaseModel


class AdminLogin(BaseModel):
    """
    管理员登陆 提交的参数信息
    "
""
    password: str = None
    username: str = None


@app.get("/on")
async def main(res: Request, sdsada: AdminLogin):
    # 迭代的方式返回流数据
    buas = await res.json()
    print("路由中单独再消费", buas)
    return PlainTextResponse('fanhui')


if __name__ == "__main__":
    import uvicorn
    import os

    app_modeel_name = os.path.basename(__file__).replace(".py", "")
    print(app_modeel_name)
    uvicorn.run(f"{app_modeel_name}:app", host='127.0.0.1', reload=True)

上面都可以吧!后续你可以自己的定义属于自己的日志中间件了!主要是纠正上一篇问题!

以上仅仅是个人结合自己的实际需求,做学习的实践笔记!如有笔误!欢迎批评指正!感谢各位大佬!

结尾

END

简书:https://www.jianshu.com/u/d6960089b087

掘金:https://juejin.cn/user/2963939079225608

公众号:微信搜【程序员小钟同学】

小钟同学 | 文 【欢迎一起学习交流】| QQ:308711822


原文作者:程序员小钟同学;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2021-02-20

  • 博文量
    224
  • 访问量
    111703