MiddlewareOnFastapiVsGin_20240801 2024-08-01

本文对比了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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容