FastAPI 是基于 Starlette 框架构建的,中间件功能直接来自 Starlette。中间件相当于套在接口外面的一层代码,每一次HTTP请求到达接口路由之前、响应返回给浏览器之前,都会经过中间件。它作用于全部请求。
什么时候使用中间件,用来做什么
适合做全请求生效的通用处理:记录请求日志、统计接口耗时、设置跨域、统一修改请求头或者响应头。
不适合写业务逻辑:接口参数校验、业务鉴权、数据库查询这类和业务强相关的逻辑不要放在中间件。
中间件基础结构
这里使用装饰器写法,最简单直观,完整模板如下:
from fastapi import FastAPI, Request
from starlette.responses import JSONResponse
app = FastAPI()
@app.middleware("http")
async def demo_middleware(request: Request, call_next):
# 这里写【预处理逻辑】:请求还没进入接口路由
print("请求来了")
# 放行请求,向后传递,拿到接口处理完成后的响应
response = await call_next(request)
# 这里写【后处理逻辑】:接口已经执行完毕,响应即将返回客户端
response.headers["X‑Demo"] = "hello"
return response
@app.get("/hello")
async def hello():
return {"msg":"hello world"}
call_next 是什么
call_next 不是我们自己定义的函数,是框架自动传入的参数。
调用 await call_next(request) 就是把当前请求放行,交给后面的逻辑。后面逻辑可能是下一个中间件,也可能是最终的接口路由函数。 程序执行到这一行的时候,会暂停当前中间件代码,去执行内层逻辑;等内层全部处理完成之后,会得到一个 response 对象,再继续执行 call_next 后面的代码。
await call_next(request)是代码分水岭
- call_next 之前:预处理,处理进来的请求
- call_next 之后:后处理,处理要返回出去的响应
放行和拦截请求
放行(正常走完整流程)
执行 response = await call_next(request),请求继续向后走到接口,拿到响应,最后 return response。上面示例就是放行。
拦截(终止请求,不走接口)
满足条件的时候,不去调用 call_next,直接返回一个响应对象。此时后续中间件、接口路由完全不会执行,请求直接结束。
注意:中间件只能返回 Response 类型对象,不能返回字典,不能返回Pydantic实例。要返回json就用JSONResponse。
拦截示例:
@app.middleware("http")
async def demo_middleware(request: Request, call_next):
# 如果访问 /block 直接拦截
if request.url.path == "/block":
return JSONResponse(content={"msg":"禁止访问"}, status_code=403)
# 其余路径正常放行
response = await call_next(request)
return response
多个中间件的执行顺序(洋葱模型)
@app.middleware("http") 装饰器注册,代码靠下方后书写的中间件,属于最外层。
示例代码:
# 写在上方,先定义mid_A
@app.middleware("http")
async def mid_A(request, call_next):
print("A预处理")
res = await call_next(request)
print("A后处理")
return res
# 写在下方,后定义mid_B
@app.middleware("http")
async def mid_B(request, call_next):
print("B预处理")
res = await call_next(request)
print("B后处理")
return res
包裹关系:mid_B( mid_A(路由) )
访问接口,打印输出:
B预处理
A预处理
# 执行接口路由
A后处理
B后处理
执行流程:
- 请求进来,先进入最外层mid_B,执行B预处理
- mid_B调用call_next,进入内层mid_A,执行A预处理
- mid_A调用call_next,进入接口路由执行业务代码
- 接口处理完成得到响应,回到mid_A,执行A后处理
- mid_A返回响应,回到mid_B,执行B后处理
- mid_B把响应返回给客户端
简单理解: 请求向内钻,从外层中间件走到接口;响应向外冒,从接口回到外层中间件。
洋葱路径图示
HTTP请求
↓
【mid_B 预处理】
↓ call_next()
【mid_A 预处理】
↓ call_next()
接口路由执行
↑ 【mid_A 后处理】
↑ 【mid_B 后处理】
↓ 返回客户端
自定义逻辑写在哪里
- 请求预处理逻辑(拿到请求,还没跑接口):写在
await call_next(request)这一行代码的前面。在这里可以做判断,满足条件直接拦截返回。 - 响应后处理逻辑(接口已经跑完,准备返回数据):写在
await call_next(request)之后,return response之前。在这里修改响应头。 - 放行:执行
await call_next(request),最后return拿到的response。 - 拦截:不调用call_next,直接return Response对象,内层后续中间件和接口路由不会执行;外层中间件的后处理仍会继续执行。
小结
- 中间件对所有http请求生效,适合做全局通用处理。
call_next是框架传入的回调,await call_next(request)用来放行请求。- call_next之前处理请求;call_next之后处理响应。
- 放行:调用call_next;拦截:不调用call_next,直接返回响应对象。
@app.middleware("http"):代码靠后的中间件为最外层;多个中间件遵循洋葱模型,请求由外到内,响应由内到外。- 中间件只能返回Response子类对象,不能返回普通字典。