本文对比了fastapi
和gin
这两个http框架在中间件设计和使用上的不同之处。相比fastapi
,gin
的单一中间件概念和中间件灵活组合的能力更胜一筹
题外 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)
进入正题,gin
和fastapi
在中间件上的差异主要有3个:中间件概念,中间件之间的数据传递方式,依赖写死与否
差异1 中间件概念
-
gin
将一个http请求的处理链路全部抽象为gin.HandlerFunc
,HandlerFunc
就是中间件,全程只有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
的用法存在两个问题:- 如果仅依赖运行,而不依赖返回值的情况下,也还是需要将被依赖函数的返回值作为参数传给主依赖函数,很丑陋。虽然丑陋但也不是不能用,真正的问题是第二点
-
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