中间件 Middleware 机制

一句话总结

中间件是“每个请求进路由前、出路由后都会经过的一道门”:统一加日志、计时、CORS、压缩、HTTPS 重定向。它像洋葱——请求从外往里穿,响应从里往外穿;后注册的中间件包在最外层。


生活类比:小区门禁 + 快递柜

把请求想成快递员进小区:

  • 请求进入时(前处理):门禁登记、测体温、分配通行码

  • 到达你家(路由):你收件

  • 响应返回时(后处理):快递柜贴条、记送达时间

中间件就是那道所有快递员都必须经过的门禁,而不是你在家门口给每个快递员单独装锁。

中间件长什么样

 
import time
 
from fastapi import FastAPI, Request
 
 
 
app = FastAPI()
 
 
 
@app.middleware("http")
 
async def add_process_time_header(request: Request, call_next):
 
    start = time.perf_counter()        # 前处理
 
    response = await call_next(request) # 把请求交给下一层(路由/内层中间件)
 
    process_time = time.perf_counter() - start
 
    response.headers["X-Process-Time"] = str(process_time)  # 后处理
 
    return response
 
  • request:进来的请求

  • call_next:一个函数,把请求传下去,返回路由生成的响应

  • call_next 改请求,在 改响应

自定义专有响应头可用 X- 前缀;要让浏览器 JS 能读到,得在 CORS 的 expose_headers 里声明。

执行顺序:洋葱模型(必考)


flowchart TD

    subgraph SGevlwj["洋葱模型 [请求方向: 外->里 | 响应方向: 里->外]"]

        B["MiddlewareB 前处理<br/>后注册=最外层"] --> A["MiddlewareA 前处理<br/>先注册=最内层"]

        A --> R[路由函数]

        R --> A2[MiddlewareA 后处理]

        A2 --> B2[MiddlewareB 后处理]

    end

代码顺序:add_middleware(A) 先,add_middleware(B) 后 →

阶段执行顺序
请求B 前 → A 前 → 路由
响应路由 → A 后 → B 后

记忆法:后注册的中间件跑前处理、跑后处理(像栈:后进先出前置,先进后出后置)。

内置中间件(都来自 Starlette)

FastAPI 继承自 Starlette,直接 app.add_middleware(...) 即可:

中间件作用
CORSMiddleware跨域(见 08-CORS跨域配置
GZipMiddleware响应压缩,省带宽
HTTPSRedirectMiddleware强制 HTTP→HTTPS
TrustedHostMiddleware限制允许访问的 Host
SessionMiddleware基于 cookie 的会话
 
from fastapi.middleware.gzip import GZipMiddleware
 
app.add_middleware(GZipMiddleware, minimum_size=1000)
 

中间件 vs 依赖注入:何时用谁?

场景用中间件用 Depends
全局统一逻辑(日志/计时/CORS)✅ 每个请求都过
特定路由需要的资源(db/当前用户)✅ 精准注入
需要访问路由函数返回值做包装✅ 后处理改响应⚠️ 较绕
需要请求级缓存/复用use_cache

简单说:中间件管“横切全局”,依赖注入管“路由专属资源”

⚠️ 与 yield 依赖、BackgroundTasks 的顺序坑

  • yield 的依赖,其收尾代码在中间件之后执行

  • BackgroundTasks(后台任务)在所有中间件之后执行

所以如果你在中间件里读了响应头、又指望后台任务改了点什么,顺序要对得上。

延伸追问

Q:中间件能改请求体吗?

A:能,但要小心——请求体是流式的,读一次就没了。需要多次读取要手动缓存(request.body() 会缓冲),注意内存。

Q:中间件和 Starlette 的关系?

A:FastAPI 几乎不实现中间件,@app.middleware("http")add_middleware 都直接来自 Starlette。FastAPI 只是帮你从 starlette.middleware.* 重新导出,方便 import。

记忆口诀

中间件 = 每请求必过的门禁,前处理+后处理。

call_next 把请求传下去,前后各插一刀。

后注册包外层,请求先进后出,响应后进先出。

全局横切用中间件,路由专属用 Depends。


▶ 对应实操:04-响应模型与状态码

▶ 对应实操:07-中间件

速记卡(面试闪卡)

Q1:一句话讲清「中间件 Middleware 机制」到底是什么?

A:中间件是每请求必经的”门禁”:进路由前和出路由后各插一刀,做日志、计时、CORS 等横切逻辑。

Q2:一、中间件长什么样、洋葱模型 —— 怎么理解? —— 怎么理解?

A:像小区门禁:每个快递员进门要登记测温(前处理),出门贴条记时间(后处理),不是你家门口给每人单装锁。代码里 @app.middleware(“http”) 包一个 async 函数,call_next 之前改请求、之后改响应。洋葱模型:请求外→里、响应里→外。英文:middleware / call_next。

Q3:二、内置中间件来自谁 —— 怎么理解? —— 怎么理解?

A:像物业配的现成门禁件:FastAPI 继承自 Starlette,add_middleware 即可挂 CORS、GZip 压缩、HTTPS 重定向、TrustedHost、Session 等。FastAPI 几乎不自己实现中间件,只是从 starlette.middleware.* 重新导出。英文:Starlette / GZipMiddleware。

Q4:三、中间件 vs 依赖注入 —— 怎么理解? —— 怎么理解?

A:像门禁和前台的区别:中间件管”所有请求都过的横切逻辑”(日志/计时/CORS);Depends 管”特定路由才需要的专属资源”(db/当前用户)。别把局部逻辑塞进全局中间件,也别用依赖注入重复做全局日志。英文:Depends / cross-cutting。

Q5:四、与 yield 依赖、BackgroundTasks 的顺序坑 —— 怎么理解? —— 怎么理解?

A:像收尾顺序有讲究:带 yield 的依赖收尾代码在中间件之后跑,BackgroundTasks 更在所有中间件之后跑。所以中间件读到的响应是后台任务执行前的快照;另外请求体是流式的、读一次就没,多次读要手动缓存。英文:yield dependency / BackgroundTasks。

Q6:核心速记主线有哪些?

  • 本质:每请求必经的门禁,前处理+后处理,call_next 传下去

  • 洋葱模型:后注册包外层,请求先进后出、响应后进先出

  • 内置件:来自 Starlette(CORS/GZip/HTTPS 重定向/Session 等)

  • 分工:中间件管横切全局,Depends 管路由专属资源

  • 顺序坑:yield 依赖、BackgroundTasks 都在中间件之后执行

口诀

A:中间件把门看,前后各插一刀鲜;

call_next 传下去,洋葱层层裹外边;

后注册包最外层,先进后出记心田;

横切全局用它对,路由专属 Depends 填。

相关链接

相关链接