4.3 中间件:CORS、GZip与Flask钩子对比


4.3 中间件:CORS、GZip 与 Flask 钩子机制对比

中间件是包裹整个应用的洋葱圈结构:请求进入时逐层穿过中间件链到达路由,响应返回时逆序穿回。CORS 头注入、响应压缩、访问日志、耗时统计这些"每个请求都要做、但不属于任何具体接口"的逻辑,都住在这一层。本节讲清 FastAPI 的两种中间件写法与执行顺序,配置 CORS 与 GZip 两个内置件,并与 Flask 的钩子体系做迁移对照。

学习目标

阅读完本节,你应当能够:

  1. 写出函数式与类式两种中间件,说明 BaseHTTPMiddleware 的适用边界;
  2. 解释中间件注册顺序与执行顺序的反向关系;
  3. 正确配置 CORSMiddleware 的五个关键参数,识别常见 CORS 报错的归属;
  4. 评估 GZip 中间件的收益场景与反向代理压缩的重复配置问题;
  5. 给出一段 Flask 钩子代码的 FastAPI 等价形式。

洋葱圈:先注册的后执行

函数式中间件的形态:

import time from fastapi import Request @app.middleware("http") async def add_timing(request: Request, call_next): start = time.perf_counter() response = await call_next(request) cost = (time.perf_counter() - start) * 1000 response.headers["X-Process-Time"] = f"{cost:.1f}ms" return response

call_next 是洋葱圈的芯:调用它意味着"把请求交给内层",await 它拿到的是内层产出的响应。call_next 之前是请求方向的逻辑(改请求头、拒 IP),之后是响应方向的逻辑(加响应头、记日志)。这段耗时统计中间件是第一个值得亲手写的例子——五分钟就能给所有接口装上耗时头,排错时极好用。

执行顺序的规则要背下来:注册得越早,位置越靠外。先 add_middleware(A) 再 add_middleware(B),请求是 A → B → 路由,响应是路由 → B → A。装饰器写法等价于把中间件加在最外层(它最后注册却包在最外面——因为装饰器始终把新中间件套在已有链外)。排错口诀:请求头相关的逻辑想最先执行就最晚注册。这与 Express 的 app.use 顺序、Django 的 MIDDLEWARE 列表顺序语义一致,从 Node 迁来的工程师可以无缝复用心智。

类式写法(继承 BaseHTTPMiddleware)适合需要状态或复杂配置的中间件,结构等价,此处不展开。需要了解的边界是:BaseHTTPMiddleware 会改变流式响应的某些行为(历史上对后台任务与流式传输的交互有坑),纯 ASGI 风格的原始签名中间件性能与兼容性最好但写法门槛高——日常用函数式,遇到流式响应的怪异问题时查一下中间件实现方式。

CORS:配置五个参数,读懂两类报错

跨域是前后端分离后的第一堵墙。FastAPI 的标准配置:

from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins=["https://app.example.com"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], )

五个参数各有语义:origins 是放行的来源列表(协议加域名加端口,缺一不可);credentials 放行 Cookie 与认证头——开启它之后 origins 不能再用星号,这是规范层的互斥,浏览器会直接拒绝;methods 与 headers 控制预检的放行范围。

排错时先分清两类失败。浏览器控制台报"未被 CORS 策略允许"且响应里没有 access-control-allow-origin 头——配置没命中:来源拼写不一致(多了斜杠、http 与 https 混用)是头号原因。 OPTIONS 请求返回 405 或 415——预检请求被路由层拒绝,通常是全局依赖或认证中间件没放行 OPTIONS 方法。第二类坑在"给 CORS 中间件内层又套了认证"时高发:中间件顺序让认证先于 CORS 执行,预检请求带着不存在的认证头进来被拒。解法是保证 CORS 在认证之外——按注册顺序规则调整即可。

Flask 世界的 flask-cors 扩展能力等价(origins、supports_credentials 等参数一一对应),迁移成本主要在把装饰器式配置改为集中式 add_middleware,反而更清爽。

GZip:中间件还是反向代理

压缩是个"两处都能做"的经典问题:

from fastapi.middleware.gzip import GZipMiddleware app.add_middleware(GZipMiddleware, minimum_size=1000)

minimum_size 设 1000 意味着小于 1KB 的响应不压——压缩开销可能超过收益。但生产环境更常见的架构是 Nginx 在前做压缩(gzip on 加类型配置),此时应用层再压就是重复劳动:CPU 白烧一遍,Accept-Encoding 的处理逻辑走两道。同一种横切关注点只该在一个层面做——有反向代理的架构把压缩交给代理,应用层中间件留给开发环境与无代理的小部署。这个判断原则适用于一大类中间件:限流在网关层还是应用层、缓存在 CDN 还是进程内,问的都是同一个问题"这个层面看得见完成这件事所需的全部信息吗"。

与 Flask 钩子的对照

Flask 的横切机制是请求生命周期钩子对:

@app.before_request def start_timer(): g.start = time.perf_counter() @app.after_request def add_header(response): response.headers["X-Process-Time"] = ... return response

FastAPI 的等价物就是前面那段耗时中间件。差异在结构:钩子对是"两个函数共享 g 对象上的状态",中间件是"一个函数里 start 变量的闭包"——后者没有共享可变对象,状态的作用域天然收窄。Flask 的 teardown_request(无论成功失败都执行)对应中间件里 try/finally 包住 call_next 的写法;Flask 的 errorhandler 对应第 4.2 节的异常处理器而非中间件。

横切需求 Flask FastAPI
请求前后逻辑 before/after_request 中间件 call_next 前后
无条件清理 teardown_request 中间件 try/finally 或依赖 yield 清理段
跨域 flask-cors 扩展 CORSMiddleware
压缩 flask-compress GZipMiddleware 或反向代理
错误格式化 errorhandler exception_handler

一个取舍提醒:能用依赖注入解决的就别上中间件。中间件包裹一切请求(包括静态文件与文档页),影响面是全局的;依赖只作用于声明它的路由,粒度精细。认证这类"部分接口需要"的逻辑用依赖(第 5 章),CORS 与耗时这类"所有请求需要"的逻辑用中间件——按影响面选机制,是两个系统并存的判断标准。

动手写一个完整的访问日志中间件

把本节知识合成一件实用工具。需求:每个请求记一行结构化日志——方法、路径、状态码、耗时、请求 id。请求 id 还要回填响应头,方便用户报障时直接提供。

import logging import time import uuid logger = logging.getLogger("access") @app.middleware("http") async def access_log(request: Request, call_next): request_id = request.headers.get("X-Request-Id") or uuid.uuid4().hex[:12] start = time.perf_counter() try: response = await call_next(request) except Exception: cost = (time.perf_counter() - start) * 1000 logger.exception("req failed", extra={ "request_id": request_id, "method": request.method, "path": request.url.path, "cost_ms": round(cost, 1), }) raise cost = (time.perf_counter() - start) * 1000 response.headers["X-Request-Id"] = request_id logger.info("req done", extra={ "request_id": request_id, "method": request.method, "path": request.url.path, "status": response.status_code, "cost_ms": round(cost, 1), }) return response

四个实现细节对应四条前文结论:请求 id 优先沿用客户端带来的值(分布式链路的常见约定),没有才生成——call_next 之前的逻辑可以读取并改写请求上下文;异常分支单独记日志再 raise,保证失败请求也有访问记录且异常继续走处理器通道(第 4.2 节的分工);extra 的结构化字段让日志可被日志系统索引(比拼字符串好用得多);耗时统计用 perf_counter 计毫秒,与本章开头那个示例同一模式。

这个中间件与 Flask 的钩子对写法对照着读一遍,两种范式的差异全部具象化:Flask 版两个函数、g 对象传状态、teardown 兜异常;FastAPI 版一个函数、闭包传状态、try 兜异常。逻辑完全相同,组织方式不同——后者把"这对钩子"合并成一个可整体阅读、整体测试的单元,这正是洋葱圈模型相对于钩子对的结构优势。

图示:洋葱圈与两种机制的分工

图示:洋葱圈与两种机制的分工

常见问题速答

**问题:中间件和异常处理器的执行顺序?**异常先被处理器翻译成响应,响应再穿中间件链返回——所以中间件的响应侧逻辑能看到错误响应(比如给 500 响应也加上耗时头)。想拦截异常本身(在处理器之前),只能在中间件的调用内层捕获——可行但通常多余,处理器已经是异常的汇聚点。

**问题:能写多个自定义中间件吗?顺序怎么定?**任意多个,顺序按注册反序包裹(正文规则)。实用组合的典型排布:最外层访问日志(看全貌)、其次 CORS(保证预检早于认证)、再次限流、最内 GZip(压缩最终产物)。这个顺序不是铁律,但日志最外、压缩最内两端固定是常见共识——前者要看到一切,后者要处理成品。

**问题:中间件里能用到依赖注入的资源吗?**不能直接用——中间件在依赖解析之前运行,拿不到按请求构造的会话与用户。中间件的世界里只有 Request 与 Response 的原始形态。需要业务资源的横切逻辑(比如按用户配额限流)用中间件加内部手动调用服务实现,或干脆改成依赖——又回到按影响面选机制的判断。

**问题:中间件拖慢了所有请求怎么办?**先测——加一层中间件在热路径上多几毫秒很正常,关键是总量。真有开销大的(外部调用的风控检查),考虑降级为异步触发(先放行后核对)或移到依赖层只作用于需要的路由。中间件的全局属性决定了它的单位成本被每个请求放大,写的时候要有这份自觉。

本节要点回顾

  • 洋葱圈模型:call_next 之前处理请求、之后处理响应;注册越早位置越外,装饰器写法始终套在最外层。
  • CORS 五参数:credentials 开启后禁用星号 origins;排错先分"配置没命中"与"预检被拒"两类。
  • GZip 取舍:有反向代理就交给代理压,同一种横切关注点只在一个层面做。
  • 钩子对照:before/after 对应中间件前后半段,teardown 对应 try/finally,errorhandler 对应异常处理器。
  • 机制选择标准:影响面全局选中间件,按需声明选依赖——认证走依赖、跨域走中间件。

正常路径与横切层齐备,第 5 章进入生产三件套:安全认证、后台任务与测试体系。

延伸与边界

中间件体系的两条边界。其一,数量边界:中间件是全局税,每加一层所有请求都付成本——务实上限是五层左右(日志、跨域、压缩、限流、一个自定义),超出时审视是否有该下沉到网关或上移到依赖的逻辑。其二,职责边界:中间件不该理解业务——它看得见请求与响应的原始形态,看不见用户与订单;一旦中间件里出现业务词汇(按订单类型改响应),就是越界的信号,该逻辑属于依赖或路由。守住两条边界,中间件层会保持"薄、快、可预测"的健康形态,成为整个服务可靠的透明外壳。

从 Flask 迁移的读者还剩最后一个心结要解:flask 钩子生态里的很多扩展(请求计数、灰度标记)在 FastAPI 里的对应物不是中间件而是依赖——影响面判断(正文反复出现的那把尺子)在迁移时比语法对照表更重要。语法可以查文档,判断力才是迁移顺利度的真正变量。

场景练习

给访问日志中间件做两个扩展练习。练习一:加上"慢请求标记"——耗时超过阈值(比如 500 毫秒)时日志级别升为警告,观察慢请求在日志流里的显形速度,这个阈值日后就是性能观测的第一道哨兵。练习二:加请求体大小记录——从请求头读长度字段记进日志,配合第 7 章的容量观测,能提前发现异常大的请求(爬虫、误用、攻击)。两个扩展各十几行,但它们让这个中间件从"练习品"变成"生产件"——好的中间件都是这样在基础版上按需长出来的,一次性设计完美中间件的尝试通常以过度工程收场。

  • 洋葱圈是资产:理解了包裹模型,限流、灰度、追踪这些未来需求都只是再包一层——中间件体系是为演进预留的扩展点。

当你能默画出请求进出洋葱圈的完整路径时,本节的目标就达成了,剩下的只是熟练度问题。

再看一眼那张洋葱圈图,然后合上它试着画一遍——能凭记忆复现的架构理解,才是真正内化了的理解。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U