MiddlewareOnFastapiVsGin_2024080

2024-07-31  本文已影响0人  9_SooHyun

本文对比了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 中间件概念

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

差异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

上一篇 下一篇

猜你喜欢

热点阅读