FastAPI自定义中间件call_next处理大JSON负载时无限阻塞问题
问题现象
部署基于BaseHTTPMiddleware实现的自定义请求日志中间件时,小体积JSON请求可以正常处理,传入大体积JSON负载时,response = await call_next(request)语句会无限期阻塞,涉及的实现代码如下:
from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request from starlette.types import Message, RequestResponseEndpoint class RequestContextLogMiddleware(BaseHTTPMiddleware): async def set_body(self, request: Request): receive_ = await request._receive() async def receive() -> Message: return receive_ request._receive = receive async def dispatch(self, request: Request, call_next: RequestResponseEndpoint): await self.set_body(request) body = await request.body() jsonbody = await request.json() id_ = jsonbody['external_id'] response = await call_next(request) return response
核心疑问:
- 大请求场景下阻塞的根本诱因是什么?
- 上述写法是否是FastAPI中读取/修改JSON请求体的正确实现?
阻塞原因
问题出在set_body方法的自定义receive实现逻辑:
- Starlette的Request对象读取请求体时,会反复调用内部的
_receive方法逐块拉取HTTP传输的分片数据,直到收到标识请求体结束的消息,才会判定请求体接收完成。小体积请求的所有数据可以在第一次_receive调用时全部返回,所以这套逻辑在小请求场景下不会暴露问题。 - 大体积JSON负载会被拆成多个TCP分片传输,示例代码里的自定义
receive函数永远只返回第一次调用_receive拿到的第一个分片,后续路由处理逻辑调用receive拉取剩余分片时,永远拿不到后续数据,也等不到请求结束的标识,就会一直挂起阻塞。
正确实现建议
上述写法不是读取/修改请求体的推荐实现,可根据场景选择对应方案:
- 若必须使用
BaseHTTPMiddleware缓存请求体,需要把所有读取到的分片按顺序存入缓存队列,自定义receive方法要按读取顺序依次返回缓存的分片,不能只存储第一次读取的单块数据。 - 优先使用原生ASGI中间件实现请求体读取/修改逻辑,避免
BaseHTTPMiddleware自带的请求上下文拷贝带来的异常;如果只是要提取请求体字段做日志、鉴权等操作,直接用FastAPI的依赖注入在路由层获取请求体即可,不需要在中间件层处理,实现更简单也不容易出问题。
内容的提问来源于stack exchange,提问作者Irfanuddin
相关产品推荐
相关产品推荐

