每个请求都要执行的逻辑放哪?FastAPI 中间件、CORS 与 lifespan

摘要:用 lifespan 管理应用启动与关闭,用 HTTP middleware 为所有响应添加请求 ID 和耗时,再通过真实预检请求验证精确的 CORS 配置。

生命周期、中间件与跨域策略作用在不同范围

图:lifespan 管理应用启停,中间件包裹每次请求,跨域策略负责浏览器来源检查。

数据库连接池只应该在应用启动时准备一次,请求 ID 和耗时应该覆盖每次请求,浏览器跨域规则则需要在路由之前统一处理。把这些逻辑复制到每个接口,既重复又容易漏。

FastAPI 为三个问题提供了不同入口:lifespan 管进程生命周期,middleware 包裹请求链路,CORSMiddleware 回答浏览器预检。下面把它们接进同一个最小应用,并分别验证。

先看响应头和跨域预检发生了什么

fastapi dev examples/ch13_middleware/main.py
curl -i -H "X-Request-ID: demo-13" http://127.0.0.1:8000/health

预期 JSON 为 {"ready":true},响应头含 X-Request-ID: demo-13X-Process-Time

先区分浏览器 CORS 与服务端安全

cd 04-fastapi-beginner
source .venv/bin/activate

浏览器 CORS 是浏览器安全机制,curl 不会主动阻止跨域请求,所以必须构造预检请求或通过前端实际验证。

分别接入 lifespan、middleware 和 CORS

声明 lifespan

@asynccontextmanager
async def lifespan(app: FastAPI):
    app.state.ready = True
    yield
    app.state.ready = False

yield 前是启动阶段,yield 后是关闭阶段。真实项目可在这里准备连接池、模型或后台资源。

添加 middleware

@app.middleware("http")
async def add_observation_headers(request, call_next):
    started_at = perf_counter()
    request_id = request.headers.get("X-Request-ID", str(uuid4()))
    response = await call_next(request)
    response.headers["X-Request-ID"] = request_id
    response.headers["X-Process-Time"] = f"{perf_counter() - started_at:.6f}"
    return response

call_next 把请求交给后续中间件或路由。前后代码分别适合请求准备和响应修饰。

配置 CORS

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],
    allow_credentials=True,
    allow_methods=["GET", "POST", "PATCH", "DELETE"],
    allow_headers=["Authorization", "Content-Type", "X-Request-ID"],
)

生产环境列出真实前端来源,不要在携带凭据时无条件放开所有来源。

生命周期、中间件和跨域配置分别接入应用

图:生命周期、中间件和跨域配置各自解决不同问题,三者并列接入应用。

构造一次真实的浏览器预检请求

发送预检请求:

curl -i -X OPTIONS http://127.0.0.1:8000/health \
  -H "Origin: http://localhost:3000" \
  -H "Access-Control-Request-Method: GET"

预期包含 access-control-allow-origin: http://localhost:3000。改成未允许的来源,比较响应头。

python -m pytest tests/test_ch13.py -q

测试退出 TestClient 上下文后还会断言 ready 已恢复为 false。

三种入口分别覆盖不同生命周期

中间件执行顺序类似洋葱:后添加与先添加的包装顺序需要通过测试确认。认证等可以用依赖精确应用到路由;请求 ID、耗时、压缩等覆盖所有请求的逻辑适合中间件。

CORS 不保护服务免受非浏览器调用,它只是告诉浏览器哪些网页来源可以读取响应。真正的安全仍依赖认证、授权、CSRF 策略和网络边界。

lifespan 比在模块导入时建立资源更可控,也更容易在测试中启动和清理。

生命周期作用于进程,中间件作用于请求,跨域由浏览器检查

图:生命周期只覆盖进程启停,中间件覆盖每次请求,跨域规则由浏览器边界检查。

横切逻辑最怕范围和顺序失控

  • 忘记 await call_next(request):请求永远到不了路由。
  • middleware 吞掉异常后返回模糊 200:应保留正确错误语义。
  • allow_origins=["*"] 同时期待凭据:需要理解浏览器限制并精确配置。
  • TestClient 不用 with:lifespan 可能不会执行。
  • 在每次请求中创建昂贵全局资源:应放到 lifespan。

把请求 ID 继续传给下游服务

  1. 未提供请求 ID 时验证服务自动生成 UUID。
  2. 增加 X-App-Version 响应头。
  3. 增加第二个允许来源并补测试。
  4. 在 lifespan 中创建一个计数器,关闭时输出最终请求数。
  5. 故意遗漏 Authorization 允许头,观察浏览器预检。

最后,用测试固定这次改动

手动请求通过后,运行与本文对应的自动化测试:

python -m pytest tests/test_ch13.py -q
python tests/validate_course.py

写在最后

应用现在拥有生命周期和横切处理能力。下一篇把请求 ID 串进日志和统一错误响应,让客户端报错信息能够与服务端日志精确关联。