FastAPI 中间件(Middleware)完全指南:请求拦截、响应增强与执行顺序
导读
中间件是 FastAPI 应用请求/响应链路中的一道“通用处理关卡”,能在任何具体路径操作(path operation)执行之前统一处理每个请求,也能在响应返回给客户端之前对每个响应做最后加工。本文以 FastAPI 官方教程的 middleware 文档(及英文原版 docs/en/docs/tutorial/middleware.md)为主线,结合仓库内 tutorial001_py310.py 的完整示例与 applications.py 的底层实现,系统讲解:中间件在请求生命周期中的位置、用 @app.middleware("http") 编写自定义中间件、在响应前后插入逻辑、以及多中间件的执行顺序与叠加原理。读完后你将能独立实现诸如耗时统计、请求日志、统一响应头注入等横切逻辑。
什么是中间件:位于每个请求和响应之间的处理函数
一个 “middleware”(中间件)本质上是这样一类函数:它在任何具体 path operation 处理请求之前,先与每一个进入应用的请求打交道;在响应被返回给客户端之前,又与每一个响应打交道。按官方文档的归纳,一次完整的中间件处理遵循如下六个步骤:
- 接收请求:中间件接收到进入应用的每个
request; - 处理请求:可以对请求执行某些操作,或运行任何必要的代码;
- 放行请求:将
request交给应用的其余部分(即某个具体的 path operation)继续处理; - 接收响应:随后拿到应用(具体 path operation)生成的
response; - 处理响应:可以对响应执行某些操作,或运行任何必要的代码;
- 返回响应:最后把
response返回给客户端。
由此可见,中间件天然适合实现那些“与具体路由无关、又需要横切整个应用”的通用能力,例如访问日志、处理耗时统计、安全校验、统一添加响应头等。
一个容易混淆的执行时机细节
文档在“Technical Details”中特别强调两点时序细节,理解它们有助于避免在生产环境中踩坑:
- 如果你的依赖项使用
yield(即依赖项在执行后释放资源的退出代码),该退出代码会运行在中间件之后; - 如果请求中注册了后台任务(详见后台任务 Background Tasks),这些后台任务会在所有中间件之后才执行。
也就是说,中间件处于“入站(请求进入)→ 路由 → 依赖退出 → 后台任务”这条时序链的最外圈偏前的位置:后台任务与依赖的收尾逻辑都严格晚于中间件执行完毕,因此中间件中为响应设置的最终状态不会被这些环节悄悄覆盖,这是设计自定义中间件时可以依赖的时序保证。
创建你的第一个 HTTP 中间件:@app.middleware("http")
创建自定义中间件最简单的方式,是在一个函数上方使用 @app.middleware("http") 装饰器。这个中间件函数接收两个参数:
request:即本次进入应用的请求对象;call_next:一个接收request作为参数的函数;- 调用
call_next(request)会把请求交给对应的 *path operation`; - 该调用最终返回由对应 *path operation
生成的response`。
- 调用
拿到 response 后,你还可以进一步修改它,最后再将修改后的 response 返回。
仓库中的完整示例 docs_src/middleware/tutorial001_py310.py 如下:
import time
from fastapi import FastAPI, Request
app = FastAPI()
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
start_time = time.perf_counter()
response = await call_next(request)
process_time = time.perf_counter() - start_time
response.headers["X-Process-Time"] = str(process_time)
return response
这段示例的核心调用关系是:
@app.middleware("http")装饰器接收类型参数"http";call_next是一个协程函数,必须使用await call_next(request)来调用——它代表了“继续走应用内部处理链”这一动作;- 中间件函数的返回值就是最终发送给客户端的
response。
类型参数与底层实现
middleware 方法的第一个参数是 middleware_type,源码(fastapi/applications.py#L4683-L4727)中明确标注:目前只支持 "http"。其内部实现为:
def decorator(func: DecoratedCallable) -> DecoratedCallable:
self.add_middleware(BaseHTTPMiddleware, dispatch=func)
return func
也就是说,@app.middleware("http") 本质上是把普通函数包装成 Starlette 的 BaseHTTPMiddleware(dispatch=func),再通过 app.add_middleware() 注册到应用中。所以从实现层面看,“装饰器写法”与下文要介绍的“app.add_middleware() 类式写法”殊途同归,最终都汇入同一条中间件注册管道。
关于 Request 类型的技术细节
示例中使用了 from fastapi import FastAPI, Request。文档特别注明:FastAPI 提供的 Request 实际上直接来自 Starlette——你也可以等价地写 from starlette.requests import Request。FastAPI 把它在自己的包内再导出,只是为了让开发者少写一层 import 的便利之举,二者是同一个类。
在 response 之前与之后插入逻辑
中间件最核心的用途,就是在一次请求处理链路的前后两段分别注入代码:
- 请求侧(前):在
request被任何 *path operation` 接收之前执行代码; - 响应侧(后):在
response被生成之后、但在把它返回给客户端之前执行代码。
上面示例中的 add_process_time_header 正是这一模式的教科书写法。对照源码文件的行号拆解其逻辑:
start_time = time.perf_counter():在请求进入路由之前记录起始时间(请求侧代码);response = await call_next(request):放行请求,等待对应路径操作完成并生成响应;process_time = time.perf_counter() - start_time:计算整个处理耗时;response.headers["X-Process-Time"] = str(process_time):把耗时以秒为单位写入自定义响应头(响应侧代码);return response:返回加工后的响应。
为什么用 time.perf_counter() 而非 time.time()
文档给出的实践建议是:此处应选用 time.perf_counter() 而不是 time.time()。原因在于 perf_counter() 使用单调时钟并以纳秒级精度为基准,不受系统时钟跳变(如 NTP 校时、手动改时间)影响,非常适合度量短时间间隔的性能计时;而 time.time() 返回墙上时钟时间,可能因系统时间调整而产生误差。对“统计单个请求处理耗时”这一典型场景,perf_counter() 是更精确的选择。事实上这一推荐同样体现在 FastAPI 官方 API 参考中 middleware 方法的文档字符串示例。
自定义头的可见性约束
示例注入的自定义头 X-Process-Time 使用了 X- 前缀,这也是业界对自定义非标准头的一种常见命名约定。
文档在此给出两条实战提示:
- 自定义专有头(custom proprietary headers)通常建议使用
X-前缀来避免与标准头冲突; - 但是,如果你希望浏览器端的 JS 客户端能读到这些自定义响应头,仅仅在后端加头还不够——还需要在 CORS 配置中使用
expose_headers参数将这些头暴露给浏览器。相关内容见 CORS(跨域资源共享) 一节,以及对应的示例源码 docs_src/cors/tutorial001_py310.py。
简言之:X-Process-Time 这样的头可以加在任意 HTTP 响应上,但“能不能被浏览器脚本读取”由 CORS 的 expose_headers 决定。
多中间件的执行顺序:后添加者在外层
实际项目中往往不止一个中间件。你可以通过 @app.middleware("http") 装饰器逐一添加,也可以使用 app.add_middleware() 方法注册,二者可以混用。它们的效果是相同的:每添加一个新中间件,都会把当前应用再包一层,形成一个“洋葱栈”。
栈的嵌套规律如下:
- 最后添加的中间件位于最外层(outermost);
- 最先添加的中间件位于最内层(innermost);
- 在请求(入站)路径上,最外层中间件最先执行;
- 在响应(出站)路径上,最外层中间件最后执行(响应依逆序逐层返回)。
例如依次执行:
app.add_middleware(MiddlewareA)
app.add_middleware(MiddlewareB)
得到的执行顺序是:
- 请求方向:
MiddlewareB → MiddlewareA → 路由 - 响应方向:
路由 → MiddlewareA → MiddlewareB
也就是说,MiddlewareB(后添加)成为最外层:请求进来先经过 B,再经过 A,最后才到达路由;而响应返回时先由内层 A 加工,再交给外层 B 加工,最后离开应用。
源码视角:中间件栈是如何构建的
“栈式封装 + 后添加者在外”这一规则可以在 FastAPI 的源码中得到印证。FastAPI 在初始化时维护 self.user_middleware 列表(fastapi/applications.py#L1014-L1018),而核心的栈构建逻辑位于 build_middleware_stack()(fastapi/applications.py#L1020-L1068):
middleware = (
[Middleware(ServerErrorMiddleware, handler=error_handler, debug=debug)]
+ self.user_middleware
+ [
Middleware(ExceptionMiddleware, handlers=exception_handlers, debug=debug),
Middleware(AsyncExitStackMiddleware),
]
)
app = self.router
for cls, args, kwargs in reversed(middleware):
app = cls(app, *args, **kwargs)
return app
关键点在于 reversed(middleware) 的逆序包装循环:内建中间件会围绕路由按 AsyncExitStackMiddleware → ExceptionMiddleware → 用户中间件(依注册顺序) → ServerErrorMiddleware 的顺序逐层包裹,最终使用户中间件列表中靠后的成员成为更外层。这与文档“最后添加的中间件最靠外”的描述完全一致。同时我们也能看到 FastAPI 在用户自定义中间件之内还保留了 ExceptionMiddleware(负责自定义异常处理器分发)与 AsyncExitStackMiddleware(负责在流式响应下正确关闭文件等资源)两层内置保障——这正是使用 app.add_middleware() / 装饰器而非裸手写 ASGI 包装器的好处:内建的服务器错误处理与异常处理器始终正常工作。
此外,仓库测试 tests/test_frontend.py 中 test_app_middleware_still_runs_for_frontend_dependencies 等用例直接断言了调用顺序数组 ["middleware-before", "dependency", "middleware-after"],从行为层面验证了“中间件环绕依赖项执行、先入先出”的可预期时序,可作为理解栈式执行顺序的实证参考。
用 app.add_middleware() 挂载现成的 ASGI / 类式中间件
装饰器写法适合自写简单函数;而注册第三方或框架内置的、以“类”形态提供的中间件时,更常用的是 app.add_middleware():
app.add_middleware(MiddlewareClass, 配置参数=值, ...)
由于 FastAPI 基于 Starlette 并完整实现了 ASGI 规范,任何遵循 ASGI 规范的中间件都可以接入。app.add_middleware() 会把 user_middleware 中的类实例化为真正的中间件对象并纳入上述 build_middleware_stack() 的包装流程,确保与内建的错误处理机制正确协作。
最典型的场景是 CORS:浏览器中的前端(运行在某一个“源”:协议 + 域名 + 端口的组合)要跨源访问后端时,后端需用 CORSMiddleware 声明允许的源、方法与头。在 CORS(Cross-Origin Resource Sharing) 一节中,其配置逻辑概括为三步:
- 导入
CORSMiddleware; - 构造一个允许源列表(字符串列表);
- 通过
app.add_middleware(CORSMiddleware, ...)把它挂到应用上。
CORSMiddleware 的默认参数是保守的,需要显式放行相应来源与能力,主要参数包括(详见该节文档):
allow_origins:允许跨源请求的源列表,例如['https://example.org', 'https://www.example.org'],也可用['*']放行任意源;allow_origin_regex:用正则匹配允许的源,例如'https://.*\.example\.org';allow_methods:允许跨源请求的 HTTP 方法,默认['GET'],['*']表示所有标准方法;allow_headers:允许跨源请求携带的请求头,默认[],['*']表示全部放行(Accept、Accept-Language、Content-Language、Content-Type对简单请求始终放行);allow_credentials:是否允许携带 Cookie 等凭据,默认False;- 以及与前文呼应、用于让浏览器端可见自定义响应头的
expose_headers。
仓库中把常见的现成中间件统一收纳在 fastapi/middleware 目录下(CORS、GZip、TrustedHost、HTTPSRedirect 等),更多逐个中间件的用法与参数可继续阅读高级用户指南:高级中间件(对应示例源码位于 docs_src/advanced_middleware,其中包含如 TrustedHostMiddleware 的 allowed_hosts / www_redirect、GZipMiddleware 等内置中间件的完整可运行代码与配套测试 tests/test_tutorial/test_advanced_middleware)。
如何验证你的中间件确实生效
本教程的中间件示例中,app 本身没有定义任何路径操作,但中间件仍会拦截一切到达应用的请求——包括 /openapi.json。仓库测试 tests/test_tutorial/test_middleware/test_tutorial001.py 恰好验证了这一点:
from fastapi.testclient import TestClient
from docs_src.middleware.tutorial001_py310 import app
client = TestClient(app)
def test_response_headers():
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert "X-Process-Time" in response.headers
该测试通过 FastAPI 自带的 TestClient 发起请求,并断言每个响应头中都存在 X-Process-Time。这说明两点:
- 中间件不依赖任何具体路由,天然具备“全应用横切”的覆盖范围;
- 写完中间件后,最直接的验证手段就是像上面这样用
TestClient检查目标响应头或响应体,无需真正启动服务器。
在你的应用代码里加入实际路径操作(例如 @app.get("/") 的根路由)后,X-Process-Time 会同样出现在这些业务接口的响应头中,即可按同样的方式编写自动化测试固化这一行为。
小结与进阶路径
本文覆盖了 FastAPI 中间件的核心知识:
- 中间件在“请求 → 路径操作 → 响应”生命周期中的六个处理环节,以及它与
yield依赖退出代码、后台任务的执行先后关系; - 用
@app.middleware("http")装饰器编写函数式中间件,其底层等价于向app.add_middleware(BaseHTTPMiddleware, dispatch=func)注册(见 fastapi/applications.py#L4723-L4725); - 借助
time.perf_counter()在call_next前后记录耗时并向X-Process-Time这类自定义头写入数据,实现“请求前 + 响应后”的双段逻辑注入; - 多中间件的洋葱栈模型与确定性的执行顺序,并了解
build_middleware_stack()(fastapi/applications.py#L1020-L1068)逆序包装的实现原理; - 用
app.add_middleware()挂载类式中间件(如 CORS),并知道自定义响应头对浏览器可见需配合expose_headers。
下一步的自然进阶是:在高级用户指南:高级中间件中学习如何挂载任意遵循 ASGI 规范的第三方中间件,以及逐一配置框架内置的 HTTPSRedirectMiddleware、TrustedHostMiddleware、GZipMiddleware 等生产常用中间件;紧接着的下一节 CORS(Cross-Origin Resource Sharing) 则会专门讲解如何用 CORSMiddleware 打通前后端跨域访问。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00