首页
/ FastAPI 中间件(Middleware)完全指南:请求拦截、响应增强与执行顺序

FastAPI 中间件(Middleware)完全指南:请求拦截、响应增强与执行顺序

2026-09-07 22:25:03作者:邬祺芯Juliet

导读

中间件是 FastAPI 应用请求/响应链路中的一道“通用处理关卡”,能在任何具体路径操作(path operation)执行之前统一处理每个请求,也能在响应返回给客户端之前对每个响应做最后加工。本文以 FastAPI 官方教程的 middleware 文档(及英文原版 docs/en/docs/tutorial/middleware.md)为主线,结合仓库内 tutorial001_py310.py 的完整示例与 applications.py 的底层实现,系统讲解:中间件在请求生命周期中的位置、用 @app.middleware("http") 编写自定义中间件、在响应前后插入逻辑、以及多中间件的执行顺序与叠加原理。读完后你将能独立实现诸如耗时统计、请求日志、统一响应头注入等横切逻辑。

什么是中间件:位于每个请求和响应之间的处理函数

一个 “middleware”(中间件)本质上是这样一类函数:它在任何具体 path operation 处理请求之前,先与每一个进入应用的请求打交道;在响应被返回给客户端之前,又与每一个响应打交道。按官方文档的归纳,一次完整的中间件处理遵循如下六个步骤:

  1. 接收请求:中间件接收到进入应用的每个 request
  2. 处理请求:可以对请求执行某些操作,或运行任何必要的代码;
  3. 放行请求:将 request 交给应用的其余部分(即某个具体的 path operation)继续处理;
  4. 接收响应:随后拿到应用(具体 path operation)生成的 response
  5. 处理响应:可以对响应执行某些操作,或运行任何必要的代码;
  6. 返回响应:最后把 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 的 BaseHTTPMiddlewaredispatch=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- 前缀,这也是业界对自定义非标准头的一种常见命名约定。

文档在此给出两条实战提示:

  1. 自定义专有头(custom proprietary headers)通常建议使用 X- 前缀来避免与标准头冲突;
  2. 但是,如果你希望浏览器端的 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.pytest_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) 一节中,其配置逻辑概括为三步:

  1. 导入 CORSMiddleware
  2. 构造一个允许源列表(字符串列表);
  3. 通过 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:允许跨源请求携带的请求头,默认 []['*'] 表示全部放行(AcceptAccept-LanguageContent-LanguageContent-Type 对简单请求始终放行);
  • allow_credentials:是否允许携带 Cookie 等凭据,默认 False
  • 以及与前文呼应、用于让浏览器端可见自定义响应头的 expose_headers

仓库中把常见的现成中间件统一收纳在 fastapi/middleware 目录下(CORS、GZip、TrustedHost、HTTPSRedirect 等),更多逐个中间件的用法与参数可继续阅读高级用户指南:高级中间件(对应示例源码位于 docs_src/advanced_middleware,其中包含如 TrustedHostMiddlewareallowed_hosts / www_redirectGZipMiddleware 等内置中间件的完整可运行代码与配套测试 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。这说明两点:

  1. 中间件不依赖任何具体路由,天然具备“全应用横切”的覆盖范围;
  2. 写完中间件后,最直接的验证手段就是像上面这样用 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 规范的第三方中间件,以及逐一配置框架内置的 HTTPSRedirectMiddlewareTrustedHostMiddlewareGZipMiddleware 等生产常用中间件;紧接着的下一节 CORS(Cross-Origin Resource Sharing) 则会专门讲解如何用 CORSMiddleware 打通前后端跨域访问。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388