fastapi中间件必须声明为async def且所有i/o操作需await,否则会阻塞事件循环;同步写法(def)导致并发能力崩溃,正确做法是await call_next(request),并避免在中间件中直接执行cpu密集型或数据库操作。

中间件必须是 async def,否则会阻塞事件循环
FastAPI 的中间件如果写成同步函数(def),哪怕只调用一次 time.sleep(0.1) 或 requests.get(),整个事件循环就会卡住——所有并发请求排队等待,吞吐量断崖式下跌。真正生效的异步中间件必须声明为 async def,且内部所有 I/O 操作都要用 await。
- 错误写法:
def my_middleware(request: Request, call_next):—— 即使里面只做日志打印,也会让后续所有异步路由失去并发能力 - 正确写法:
async def my_middleware(request: Request, call_next): -
call_next本身返回的是一个协程,必须await call_next(request),不能直接 return - 常见陷阱:在中间件里用
json.loads()解析 request.body() 返回的 bytes —— 这是 CPU 密集型操作,应改用await request.json()(它已封装为异步)
如何安全地在异步中间件中读取 request body
直接调用 await request.body() 后,request 流会被消耗一次;若后续路由或依赖项再次尝试读取(比如 await request.json()),会得到空数据或抛出 Request body not found 错误。
- 解决方案:用
await request.body()读取后,手动重建 request,或更稳妥地使用Starlette提供的Request.scope克隆机制 - 推荐做法:只在必要时读 body(如鉴权、审计日志),并缓存结果到
request.state中供后续使用:request.state.raw_body = await request.body() - 避免在中间件里做耗时 JSON 解析,可先用
await request.body()获取 raw bytes,再用asyncio.to_thread(json.loads, ...)转移 CPU 工作到线程池
异步中间件 + 数据库连接池容易踩的坑
很多团队想在中间件里统一记录请求耗时、用户 ID、SQL 查询数等指标,顺手加个数据库写入 —— 这极易引发连接泄漏或死锁,尤其当使用 SQLAlchemy 2.0+ AsyncSession 时。
- 错误模式:每次中间件都
await session.execute(...)写日志,但没确保 session 正确关闭或返回连接池 - 正确姿势:中间件内不直接管理 DB session;改为将日志数据暂存
request.state.log_buffer,在路由结束后通过BackgroundTasks或响应后钩子异步提交 - 若真需同步写(如关键审计),务必使用独立的、带超时和最大连接数限制的专用连接池,与业务数据库池隔离
- 注意:
BackgroundTasks.add_task()不保证执行完成就返回响应,但能避免阻塞主流程 —— 日志丢失风险需自行兜底(例如落本地文件+定时重试)
gzip 和 cors 中间件是否需要改造?
不需要。FastAPI 内置的 GZipMiddleware 和 CORSMiddleware 本身就是异步实现,源码里全是 async def,直接 add_middleware 即可,无需任何修改。
- 验证方式:查看
fastapi/middleware/gzip.py和starlette/middleware/cors.py,确认其dispatch方法是 async - 但自定义中间件(比如统计中间件、token 解析中间件)必须自己保证异步性
- 第三方中间件要逐个检查源码或文档,明确标注 “supports ASGI” 或 “async compatible” 才可信
GZipMiddleware 之后,那压缩前的响应体大小就拿不到;放在认证中间件之前,则无法访问 request.state.user。顺序不是性能问题,而是逻辑正确性的前提。大量免费API接口:立即使用
涵盖生活服务API、金融科技API、企业工商API、等相关的API接口服务。免费API接口可安全、合规地连接上下游,为数据API应用能力赋能!











