创建时间: 2026-08-25最后更新: 2026-08-27

1. 中间件

依赖通常服务于某个路径或一组路径,中间件则包裹整个 HTTP 请求处理过程。每个请求先经过中间件,再进入路由;路由生成响应后,响应再沿着中间件链返回客户端。

中间件适合处理横跨多数接口的工作:

  • 请求耗时;
  • Request ID;
  • 统一日志上下文;
  • CORS;
  • 安全响应头;
  • 压缩和可信主机检查。

它不适合承载具体的聊天、订单或用户业务。

2. 自定义 HTTP 中间件

使用 @app.middleware("http") 可以声明 HTTP 中间件:

app/middleware/timing.py
01
import time
02
03
from fastapi import Request
04
05
06
async def add_process_time(request: Request, call_next):
07
"""记录请求处理耗时,并把结果写入响应头。"""
08
09
started_at = time.perf_counter()
10
response = await call_next(request)
11
duration = time.perf_counter() - started_at
12
response.headers["X-Process-Time"] = f"{duration:.6f}"
13
return response
NOTE

项目位置:app/middleware/timing.py,由 app/middleware/__init__.py 注册,最终在 app/factory.py 中启用。

request 是当前请求,call_next(request) 把请求交给下一层中间件或路由。等待响应回来后,当前中间件还可以修改响应头。

如果这个中间件需要继续处理后续路由,通常应调用并返回 call_next() 的结果。认证、限流或维护模式等中间件也可以主动拦截请求,直接返回一个 Response,这时不需要调用 call_next()。

如果后续代码抛出未处理异常,await call_next() 本身也会抛出,示例中设置响应头的代码不会执行。需要无论成功失败都记录耗时时,应使用 try/finally 写日志;但在 finally 中可能没有成功响应对象,不能强行添加响应头。

@app.middleware("http") 适合大多数普通 HTTP 横切逻辑。它底层使用 Starlette 的 BaseHTTPMiddleware;如果需要依赖 ContextVar 传播、精确观察 ASGI 消息或处理复杂流式场景,应进一步了解纯 ASGI 中间件。这个装饰器只处理 HTTP 请求,不会自动处理 WebSocket。

3. Request ID

Request ID 可以把客户端错误、接口日志和上游调用关联起来:

app/middleware/request_id.py
01
import re
02
from uuid import uuid4
03
04
from fastapi import Request
05
06
07
request_id_pattern = re.compile(r"^[A-Za-z0-9._:-]{1,128}$")
08
09
10
async def add_request_id(request: Request, call_next):
11
"""复用合法的客户端 Request ID,否则生成新的随机 ID。"""
12
13
candidate = request.headers.get("X-Request-Id", "")
14
request_id = (
15
candidate
16
if request_id_pattern.fullmatch(candidate)
17
else uuid4().hex
18
)
19
request.state.request_id = request_id
20
21
response = await call_next(request)
22
response.headers["X-Request-Id"] = request_id
23
return response
NOTE

项目位置:app/middleware/request_id.py,请求 ID 会写入 request.state.request_id,并由 app/middleware/__init__.py 注册。

request.state 只属于当前请求,可以被后续依赖和路径函数读取。示例只接受长度和字符均受限的外部 Request ID,否则生成新的随机值,避免恶意内容污染日志。

这个示例只在成功取得 response 后写入响应头。路径函数抛出未处理异常时,应由异常处理器读取 request.state.request_id,在日志或错误响应中继续使用同一个 ID。Request ID 不是用户身份,也不能替代分布式追踪的 Trace ID,它只是一次请求的关联标识。

4. 中间件执行顺序

多个中间件会形成嵌套结构。不要只依赖占位名称推断顺序,当前项目的注册代码如下:

app/middleware/__init__.py
01
from fastapi import FastAPI
02
from fastapi.middleware.cors import CORSMiddleware
03
04
from app.core.config import Settings
05
06
from .request_id import add_request_id
07
from .security_headers import add_security_headers
08
from .timing import add_process_time
09
10
11
def register_middleware(app: FastAPI, settings: Settings) -> None:
12
"""按统一顺序注册应用级中间件。"""
13
14
app.middleware("http")(add_security_headers)
15
app.middleware("http")(add_request_id)
16
app.middleware("http")(add_process_time)
17
18
# 最后注册 CORS,使它位于最外层,错误响应也能获得跨域响应头。
19
app.add_middleware(
20
CORSMiddleware,
21
allow_origins=settings.allowed_origins,
22
allow_credentials=settings.allow_credentials,
23
allow_methods=settings.cors_allow_methods,
24
allow_headers=settings.cors_allow_headers,
25
expose_headers=settings.cors_expose_headers,
26
)
NOTE

项目位置:app/middleware/__init__.py。这里集中注册 CORS、响应安全头、Request ID 和耗时中间件。

当前项目的执行顺序是(CORS 最后注册,因此位于最外层):

code.ts
1
请求:CORS -> 耗时 -> Request ID -> 安全响应头 -> 路由
2
响应:路由 -> 安全响应头 -> Request ID -> 耗时 -> CORS

顺序会影响日志、异常、压缩和 CORS 响应。增加中间件时,应明确它需要观察原始请求,还是需要包住其他中间件产生的响应。

在 FastAPI 0.118.0 及以上版本中,带 yield 的依赖默认使用 scope="request",会在响应发送之后执行退出代码;FastAPI 0.121.0 起支持的 scope="function" 则会在路径函数结束、响应发送之前清理资源。后台任务会在所有中间件完成后运行。因此中间件记录的结束时间通常不包含后台任务,资源清理的具体时机也不能只靠看到路径函数 return 来推断。旧版 FastAPI 的 yield 依赖时机有所不同,升级项目时应确认版本行为。

5. CORS 配置

CORS 是浏览器的跨源读取规则。源由协议、域名和端口共同决定:

code.ts
1
https://langchainpy.usehook.cn
2
http://127.0.0.1:8000

这两个地址的协议、域名和端口都不同,因此属于不同源。网页 JavaScript 调用本地 FastAPI 时,浏览器会检查服务是否允许该网页来源。

FastAPI 可以添加 CORSMiddleware:

cors.py
01
from fastapi import FastAPI
02
from fastapi.middleware.cors import CORSMiddleware
03
04
app = FastAPI()
05
06
app.add_middleware(
07
CORSMiddleware,
08
allow_origins=[
09
"https://langchainpy.usehook.cn",
10
"http://localhost:3000",
11
],
12
allow_credentials=False,
13
allow_methods=["GET", "POST", "OPTIONS"],
14
allow_headers=[
15
"Content-Type",
16
"X-Local-Token",
17
"X-Request-Id",
18
],
19
expose_headers=["X-Request-Id"],
20
)
NOTE

项目位置:CORS 的来源和方法配置在 app/core/config.py,实际注册在 app/middleware/__init__.py。文章示例使用 allow_credentials=False,适用于不携带 Cookie 的公开跨域接口;当前项目在来源为 * 时自动关闭凭证,只有配置明确来源时才允许凭证。

需要限制来源或保护私有数据时,生产配置应列出真实来源,不要因为调试方便就长期使用 allow_origins=["*"]。如果允许 Cookie 或其他凭证,还要同时理解 allow_credentials、SameSite 和浏览器凭证模式;此时 allow_origins、allow_methods 和 allow_headers 都不能使用通配配置。

6. CORS 预检

跨域请求在真正发送前,浏览器可能先发送 OPTIONS 预检请求,询问服务器是否允许当前来源、方法和请求头。

因此,看到两次网络请求不一定是重复调用:

code.ts
1
OPTIONS /api/v1/chat
2
POST /api/v1/chat

CORSMiddleware 会处理符合规则的预检请求。不要为每个路径手写 OPTIONS 路由,也不要把预检失败误判为模型接口失败。

可以用 curl 单独检查预检是否通过,而不必真的调用模型:

cors-preflight.sh
1
curl -i -X OPTIONS http://127.0.0.1:8000/api/v1/chat \
2
-H 'Origin: https://langchainpy.usehook.cn' \
3
-H 'Access-Control-Request-Method: POST' \
4
-H 'Access-Control-Request-Headers: content-type,x-local-token'

CORS 只限制浏览器是否允许网页代码读取跨源响应。curl、服务器程序和恶意本地程序不受浏览器 CORS 保护,因此 CORS 不是认证和防火墙。

浏览器报 CORS 错误时,根因不一定是 Origin 列表。服务没有启动、TLS 失败、预检请求被代理拦截、请求头未被允许,或者服务先返回了不带 CORS 头的 500,都可能在前端表现为「被 CORS 拦截」。应先在 Network 面板查看 OPTIONS 和实际请求各自的状态。若要求未处理异常也带 CORS 响应头,可以把 CORSMiddleware 包在整个应用外层,避免被更外层的 500 处理器挡住。

7. 公网页面访问本地服务

本小册的页面运行在公网,Python 服务运行在读者电脑。配置本地服务时应遵守几个边界:

  • 默认只监听 127.0.0.1,不要无意暴露到局域网;
  • 只允许明确的课程站点和本地开发站点 Origin;
  • 模型 API Key 保留在 Python 服务的 .env 中;
  • 网页只保存本地服务地址和必要的临时配对凭证;
  • 对请求体、文件大小、并发和执行时间设置限制。

公网页面访问本地网络时,浏览器还可能执行 Private Network Access(PNA)检查:预检请求会带上 Access-Control-Request-Private-Network: true。如果确实需要允许这种访问,应在确认威胁模型后配置 allow_private_network=True,并继续使用本地令牌保护会产生副作用的接口。CORS 配置正确只说明服务端允许该 Origin,不保证浏览器的其他安全层一定放行。

8. 中间件与依赖的边界

两者可以这样选择:

需求更适合
所有请求统一计时中间件
添加 Request ID中间件
CORS官方中间件
当前用户依赖
数据库 Session带 yield 的依赖
某组路由的权限Router 依赖
聊天业务Service

中间件只能看到相对底层的 Request 和 Response,不会自动得到路径函数已经解析好的 Pydantic 参数。需要业务字段时,通常应在依赖或路径函数中处理。

9. 流式响应中的中间件

中间件处理流式响应时要格外小心。call_next() 返回不代表流已经全部发送,响应体可能在之后持续迭代。

不要为了记录内容而在普通中间件中把流式响应体完整读入内存,这会破坏逐块发送,并可能造成巨大的内存占用。call_next() 返回响应对象时,流式正文可能还没有发送完成;要统计首块时间或字节数,应包装响应迭代器或底层 ASGI 的 send,在每个响应块经过时记录数据。记录流式接口时,更适合观察状态、首块时间、总时长、结束原因和字节数,而不是强行缓存全部正文。

10. 响应安全头

简单服务可以通过中间件增加必要响应头,但安全策略要结合部署方式:

app/middleware/security_headers.py
01
from fastapi import Request
02
03
04
async def add_security_headers(request: Request, call_next):
05
"""为 API 响应添加不会依赖部署环境的基础安全头。"""
06
07
response = await call_next(request)
08
response.headers.setdefault("X-Content-Type-Options", "nosniff")
09
if request.url.path.startswith("/api/"):
10
response.headers.setdefault("Cache-Control", "no-store")
11
return response
NOTE

项目位置:app/middleware/security_headers.py。这里将 Cache-Control: no-store 限定在 /api/,避免把文档或静态资源也错误地标记为不可缓存。

Cache-Control: no-store 适合包含私人模型回复的接口,却不一定适合静态资源或公共查询。不要把同一套响应头不加判断地应用到所有内容。

FastAPI 还可以复用 Starlette 提供的常用中间件:

中间件用途需要注意
TrustedHostMiddleware限制 Host 请求头不能替代网络访问控制
HTTPSRedirectMiddleware重定向到 HTTPS本地回环开发通常不启用
GZipMiddleware压缩响应会影响流式数据的传输特征;当前版本默认不压缩 text/event-stream

中间件越多不代表越安全。每一层都会改变请求链,应只添加有明确威胁模型或性能收益的能力,并通过测试确认错误响应、预检和流式输出仍然正常。

NOTE

项目位置说明:当前项目没有在 app/middleware/__init__.py 默认启用 TrustedHostMiddleware、HTTPSRedirectMiddleware 或 GZipMiddleware。本地服务使用回环 HTTP,强制 HTTPS 会妨碍开发;TestClient 使用的 Host 也不适合直接套生产白名单;压缩则可能改变流式响应特征。部署到明确的生产环境后,再根据 Host 白名单、TLS 终止位置和响应类型逐项启用。

11. 串联使用

前面的能力通常会在应用工厂中一起注册。下面的最小示例把 CORS、Request ID 和耗时响应头放进同一个应用,业务路由仍然只负责业务本身:

compose-middleware.py
01
import time
02
from uuid import uuid4
03
04
from fastapi import FastAPI, Request
05
from fastapi.middleware.cors import CORSMiddleware
06
07
08
def create_app() -> FastAPI:
09
app = FastAPI()
10
11
app.add_middleware(
12
CORSMiddleware,
13
allow_origins=["https://langchainpy.usehook.cn"],
14
allow_credentials=False,
15
allow_methods=["GET"],
16
allow_headers=[],
17
expose_headers=["X-Request-Id", "X-Process-Time"],
18
)
19
20
@app.middleware("http")
21
async def add_request_context(request: Request, call_next):
22
started_at = time.perf_counter()
23
request_id = uuid4().hex
24
request.state.request_id = request_id
25
26
response = await call_next(request)
27
response.headers["X-Request-Id"] = request_id
28
response.headers["X-Process-Time"] = (
29
f"{time.perf_counter() - started_at:.6f}"
30
)
31
return response
32
33
@app.get("/health")
34
async def health() -> dict[str, str]:
35
return {"status": "ok"}
36
37
return app
38
39
40
app = create_app()

这里先添加的 CORSMiddleware 位于内层,后声明的 add_request_context 位于外层,因此 Request ID 和计时逻辑会包住 CORS 处理。真正的项目还应把外部 Request ID 校验、异常处理、日志脱敏和流式响应策略补齐。

NOTE

项目位置:当前项目没有在 compose-middleware.py 中重复创建应用,而是由 app/factory.py 调用 register_middleware(app, settings),具体注册实现位于 app/middleware/__init__.py。这里的组合示例使用固定来源和 allow_credentials=False,作为独立教学片段是合理的;接入项目时应改为集中配置。

正在验证登录状态
请稍候,验证完成后将继续显示文章内容