美文网首页
MiddlewareOnFastapiVsGin_2024080

MiddlewareOnFastapiVsGin_2024080

作者: 9_SooHyun | 来源:发表于2024-07-31 20:07 被阅读0次

    本文对比了fastapigin这两个http框架在中间件设计和使用上的不同之处。相比fastapigin单一中间件概念中间件灵活组合的能力更胜一筹

    题外 fastapi需要额外注册exception handle logic

    fastapi是python框架,python框架是try except finally式的错误处理,路由核心处理函数可能继续向上抛exception,不像golang是显式实时的错误处理,因此在使用层面通常需要额外在api层注册兜底的exception handle logic。如下:

    from fastapi import FastAPI, Request
    from fastapi.responses import JSONResponse
    from fastapi.exceptions import RequestValidationError
    from .resp_constructor import create_failed_response
    
    
    async def validation_exception_handler(request: Request, exc: RequestValidationError):
        return JSONResponse(
            status_code=400,
            content=create_failed_response(
                code=400, msg="request Validation Error", data=exc.errors()).model_dump()
        )
    
    
    async def global_exception_handler(request: Request, exc: Exception):
        return JSONResponse(
            status_code=500,
            content=create_failed_response(
                code=500, msg="Internal Server Error.", data=str(exc)).model_dump()
        )
    
    
    def add_exception_handler(app: FastAPI):
        """
        注册exception_handler
        """
        app.add_exception_handler(RequestValidationError,
                                  validation_exception_handler)
        app.add_exception_handler(Exception, global_exception_handler)
    
    

    进入正题,ginfastapi在中间件上的差异主要有3个:中间件概念中间件之间的数据传递方式依赖写死与否

    差异1 中间件概念

    • gin将一个http请求的处理链路全部抽象为gin.HandlerFuncHandlerFunc就是中间件,全程只有HandlerFunc一个概念,做鉴权、参数校验、业务逻辑处理等

      // ...
      hostRead.POST("DescribeHost", h.Exist, h.DescribeHost)
      hostRead.POST("ListHostHardwares", h.Exist, h.ListHostHardwares)
      // ...
      

      h.Exist h.DescribeHost都是HandlerFunc。some kinds of middleware run after the request routing, and can be freely arranged and combined

    • fastapi区分了中间件核心路由处理函数依赖函数3个概念。

      • 中间件。通常做一些全局的公共动作
        import uuid
        from fastapi import Request, FastAPI
        from starlette.middleware.base import BaseHTTPMiddleware
        from starlette.responses import Response
        from .context_vars import trace_id_var
        class TraceIDMiddleware(BaseHTTPMiddleware):
            """
            请求进来就生成trace_id
            """
        
            async def dispatch(self, request: Request, call_next) -> Response:
                trace_id = str(uuid.uuid4())
                trace_id_var.set(trace_id)
                response = await call_next(request)
                response.headers['X-Trace-ID'] = trace_id
                return response
        
        
        def register_middlewares(app: FastAPI):
            # 注册中间件. stack顺序:最后注册的中间件最先执行
            app.add_middleware(TraceIDMiddleware)
        
        
      • 路由处理函数依赖函数。在FastAPI中,路由处理函数和依赖函数都可以接受请求参数(如查询参数、路径参数、请求体等)。FastAPI会自动解析这些参数并将它们传递给相应的函数。
        import json
        from fastapi import Depends, FastAPI, HTTPException, Body
        from pydantic import BaseModel
        
        app = FastAPI()
        
        
        class ReqInput(BaseModel):
            user_id: int
        
        
        def verify_user(req: ReqInput=Body(...)):
            """
            依赖函数:校验用户是否存在
            """
            user_exists = req.user_id == 123
            if not user_exists:
                return False
            return True
        
        
        @app.post("/delete")
        def delete_user(req: ReqInput = Body(...), verified: bool = Depends(verify_user)):
            """
            路由处理函数: 依赖于verify_user
            """
            if not verified:
                raise HTTPException(status_code=404, detail="User not found")
            return {"message": f"User {req.user_id} deleted"}
        
        
        if __name__ == "__main__":
            import uvicorn
            uvicorn.run(app, host="0.0.0.0", port=9999)
        
            # test curl
            # curl -XPOST 127.0.0.1:9999/delete -d '{"user_id": 123}' -H "content-type:application/json"
        
        
        通过Depends方法声明依赖关系,依赖函数也可以通过Depends去依赖另一个函数。Depends的本质就是一次函数调用,和普通的在函数body内部执行调用一摸一样...使用Depends反而还有点四不像,直接在函数body内调用就好了:def delete_user(req: ReqInput = Body(...)): pass = verify_user(req)

    差异2 中间件之间的数据传递方式

    • gin中,所有HandlerFunc往同一个gin.context对象中存取数据,每个HandlerFunc可以自由地按需存取数据

    • fastapi的路由函数和依赖函数之间通过函数的调用和返回来进行值传递。如下:

      from fastapi import Depends, FastAPI, HTTPException, Body
      from pydantic import BaseModel
      
      app = FastAPI()
      
      
      class ReqInput(BaseModel):
          user_id: int
          op_role: str
      
      
      """
      路由处理函数 和 依赖函数 的整体依赖关系如下:
      
      delete_user -> verify_user -> op_auth -> most_basic_dependency
                                -> most_basic_dependency
      """
      
      
      def most_basic_dependency():
          """
          最底层的依赖函数
          """
          print("most_basic_dependency runs")  # 用于演示most_basic_dependency的执行次数
          return None
      
      
      def op_auth(req: ReqInput = Body(...), nop_depend=Depends(most_basic_dependency, use_cache=False)):
          """
          校验操作人是否具有管理员权限
      
          op_auth作为被依赖函数,在核心路由处理函数前执行。
          同时,op_auth依赖于most_basic_dependency。但是,【op_auth仅依赖most_basic_dependency的执行,而不依赖其返回值】。
          因此nop_depend参数无用,但是为了调用依赖,必须使用这种丑陋的写法
          """
          auth = req.op_role == "admin"
          if not auth:
              raise HTTPException(status_code=400, detail="Unauthorized")
          print("Authorized")
          return None
      
      
      def verify_user(req: ReqInput = Body(...), nop_depend1=Depends(most_basic_dependency, use_cache=False), nop_depend2=Depends(op_auth, use_cache=False)):
          """
          校验用户是否存在
      
          verify_user作为依赖函数,在核心路由处理函数前执行
          nop_depend1/nop_depend2同样是无用参数,仅为了调用依赖
          """
          user_exists = req.user_id == 123
          if not user_exists:
              return False
          return True
      
      
      @app.post("/delete")
      def delete_user(req: ReqInput = Body(...), verified: bool = Depends(verify_user)):
          if not verified:
              raise HTTPException(status_code=404, detail="User not found")
          return {"message": f"User {req.user_id} deleted"}
      
      
      if __name__ == "__main__":
          import uvicorn
          uvicorn.run(app, host="0.0.0.0", port=9999)
      
          # test curl
          # curl -XPOST 127.0.0.1:9999/delete -d '{"user_id": 123, "op_role": "admin"}' -H "content-type:application/json"
      
      

      示例代码中,每个Depends调用一个依赖函数,并且将返回值传递给主调的一个参数。

      Depends的用法存在两个问题:

        1. 如果仅依赖运行,而不依赖返回值的情况下,也还是需要将被依赖函数的返回值作为参数传给主依赖函数,很丑陋。虽然丑陋但也不是不能用,真正的问题是第二点
        1. Depends的使用写死了每个中间件的上下游依赖关系,导致中间件之间无法像gin.HandlerFunc一样自由地排列组合。如,op_auth的逻辑只和op_role有关系,在示例代码中入参是ReqInput,这时另外一个同样需要op_auth但参数不同的api就无法使用op_auth(req: ReqInput)了。verify_user同理

      这也引出了它们之间的差异3:依赖是否写死

    差异3 依赖是否写死

    显然,Depends的使用写死了每个中间件的上下游依赖关系

    但我们可以参考gin.HandlerFunc的思路,让所有依赖函数从共同的对象读写请求数据,这样每个依赖函数都只依赖公共数据,而不会产生相互依赖。fastapi实现:将请求body字典化后,在各种Depends方法中作为参数传递,让依赖函数变得通用

    具体实现如下:

    import json
    from fastapi import Depends, FastAPI, HTTPException, Body, Request
    from pydantic import BaseModel
    
    app = FastAPI()
    
    
    class ReqInput(BaseModel):
        user_id: int
        op_role: str
    
    
    """
    路由处理函数 和 依赖函数 的整体依赖关系如下:
    
    delete_user -> verify_user -> op_auth -> to_dict
    """
    
    
    async def to_dict(req: Request):
        """
        通用方法,将请求体转换为字典
        """
    
        # req.body() 是一个异步方法,返回一个协程对象
        body = await req.body()
        # 将请求体转换为字典
        try:
            request_body_dict = json.loads(body)
        except json.JSONDecodeError:
            request_body_dict = {}
        return request_body_dict
    
    
    def op_auth(context_param: str = Depends(to_dict)):
        """
        校验操作人是否具有管理员权限
    
        依赖于to_dict,to_dict是通用的,因此op_auth是通用的
        """
        op_role = context_param["op_role"]
        auth = op_role == "admin"
        if not auth:
            raise HTTPException(status_code=400, detail="Unauthorized")
        print("Authorized")
        return None
    
    
    def verify_user(context_param: str = Depends(to_dict), nop_depend=Depends(op_auth)):
        """
        校验用户是否存在
    
        依赖于to_dict,to_dict是通用的,因此verify_user是通用的
        """
        user_id = context_param["user_id"]
        user_exists = user_id == 123
        if not user_exists:
            return False
        return True
    
    
    @app.post("/delete")
    def delete_user(req: ReqInput = Body(...), verified: bool = Depends(verify_user)):
        """
        让路由处理函数仅依赖于通用方法,实现类似golang `gin.HandlerFunc`一样自由地排列组合的效果
        """
        if not verified:
            raise HTTPException(status_code=404, detail="User not found")
        return {"message": f"User {req.user_id} deleted"}
    
    
    if __name__ == "__main__":
        import uvicorn
        uvicorn.run(app, host="0.0.0.0", port=9999)
    
        # test curl
        # curl -XPOST 127.0.0.1:9999/delete -d '{"user_id": 123, "op_role": "admin"}' -H "content-type:application/json"
    

    to_dict op_auth verify_user都是通用方法,to_dict生产公共数据,op_auth verify_user仅读写公共数据,没有其他依赖,可以方便地被其他路由复用

    总结

    对比来看,gin的设计更加简洁

    一个gin.HandlerFunc概念就收敛了fastapi中的中间件、核心路由处理函数和依赖函数这几个概念,而且它的流水线式的Handler调用让其Handler非常通用,可以灵活根据业务需要排列组合实现功能

    关于中间件的思考,回见https://www.jianshu.com/p/50e4898512ac

    相关文章

      网友评论

          本文标题:MiddlewareOnFastapiVsGin_2024080

          本文链接:https://www.haomeiwen.com/subject/pxwnhjtx.html