FastAPI中间件本质是洋葱模型的异步拦截器,通过@app.middleware("http")或继承BaseHTTPMiddleware实现,在请求进入前、响应返回后执行逻辑,支持鉴权短路、日志、CORS等全局处理。

为什么直接用 aiohttp 或 FastAPI 的中间件不够用?
因为它们默认只支持「请求进入 → 处理 → 响应返回」的单向链路,而真正需要中间件拦截、改写、短路(比如鉴权失败直接返回 401)时,你得清楚每个中间件的执行顺序和生命周期钩子。比如 aiohttp 的 on_response_prepare 是响应已生成但尚未发送时触发,没法修改响应体;而 FastAPI 的依赖注入式中间件不支持在响应流中动态插入数据。
如果你要实现类似 Express.js 那种 next() 控制权移交、或 Django 中间件的 process_request/process_response 分离逻辑,就得自己搭骨架。
用 asyncio + async def 手写中间件链的关键点
核心是把中间件设计成接收 request 和 call_next(下一个中间件或路由处理器)的协程函数,并确保调用链能正确 await。
- 每个中间件必须是
async def,且最后一行必须是await call_next(request)(除非你主动短路) -
call_next不是固定函数,而是由上一层中间件动态传入的闭包,不能硬编码调用 - 请求对象最好用
dict或简单dataclass封装,避免依赖框架特定类型(比如aiohttp.web.Request),否则中间件复用性差 - 错误处理要显式
try/except包裹await call_next(...),否则异常会跳出中间件链,上层捕获不到
示例中间件:
立即学习“Python免费学习笔记(深入)”;
async def auth_middleware(request, call_next):
if not request.get("headers", {}).get("Authorization"):
return {"status": 401, "body": "Unauthorized"}
try:
return await call_next(request)
except Exception as e:
return {"status": 500, "body": str(e)}
uvicorn 自定义 ASGI app 时如何注入中间件?
不要试图 patch Uvicorn 内部逻辑,而是实现一个符合 ASGI 规范的 callable,它本身就是一个中间件容器。
- ASGI app 签名是
app(scope, receive, send),其中scope包含请求信息,receive/send是异步消息通道 - 你的顶层中间件要包装原始 app,并在
receive后解析请求,在send前修改响应,但注意:ASGI 要求send必须被调用一次且仅一次,不能多次 send status/body - 如果中间件需读取完整 body(比如做签名验证),得先
await receive()直到"more_body": False,否则后续 app 拿不到 body - 使用
starlette.middleware.base.BaseHTTPMiddleware是更稳妥的选择——它内部已处理了 scope 复制、send 包装、异常传播等细节,比裸写 ASGI 更少出错
中间件顺序错了会导致什么?
顺序决定执行时机和可操作范围。比如日志中间件放在鉴权前面,就可能记录未授权请求;而压缩中间件如果放在 JSON 序列化之前,就会压缩原始 dict 而非 bytes,导致出错。
- 鉴权类中间件(如检查 token)必须在业务逻辑前,且应能短路整个链
- 日志、监控类中间件适合放首尾:首层记录请求开始,末层记录响应结束(需注意响应体可能已被流式发送,无法重复读)
- 响应修改类(如加 CORS header)必须在业务 handler 之后、实际
send之前,否则 header 会被覆盖 - 多个中间件嵌套时,
await call_next()的返回值必须统一为可被下一级消费的格式(比如都返回dict或都返回ASGI兼容的send调用),混用会导致类型错误
最常被忽略的是中间件对流式响应(如 SSE、大文件下载)的兼容性——一旦中间件尝试读取全部响应体再修改,就破坏了 streaming 语义,内存暴涨甚至阻塞。


















