You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Starlette中间件读取请求体致504阻塞,如何安全读取请求体?

解决FastAPI中间件读取请求体后请求冻结的问题

这个问题我之前也碰到过——原因很简单:FastAPI的Request.body()方法是一次性消耗的,你读取完请求体之后,原始的请求流就被耗尽了,后面的路由函数再尝试解析请求体(比如绑定Pydantic模型)的时候,就会一直等待数据,最终超时返回504。

要安全读取请求体同时不影响后续处理,核心思路是:读取请求体后,把内容备份回Request对象,让后续流程还能获取到。这里有两种常用的实现方式:

方法一:直接设置_body属性(简单高效)

FastAPI的Request内部会优先检查_body私有属性是否存在,如果存在就直接返回该值,不用再从流中读取。所以我们只需要在读取后把body赋值给这个属性即可:

class RequestContext(BaseHTTPMiddleware): 
    async def dispatch(self, request: Request, call_next: RequestResponseEndpoint): 
        request_id = request_ctx.set(str(uuid4())) # 为请求生成UUID
        
        # 读取请求体
        body = await request.body() 
        # 关键:把读取后的请求体备份回Request对象
        request._body = body
        
        # 记录请求日志
        if body: 
            # 解码成字符串(根据实际请求编码调整,比如表单可能用gbk)
            logger.info(f"Request [{request_id}] | Path: {request.url.path} | Body: {body.decode('utf-8')}")
        else: 
            logger.info(f"Request [{request_id}] | Path: {request.url.path} | No body")
        
        # 继续处理请求
        response = await call_next(request) 
        response.headers['X-Request-ID'] = request_ctx.get() 
        logger.info(f"Request [{request_id}] | Status: {response.status_code}") 
        
        request_ctx.reset(request_id) 
        return response

方法二:替换receive方法(更规范的私有属性兼容方案)

如果你担心直接操作私有属性_body在未来FastAPI版本中出现兼容性问题,可以通过替换Request的_receive方法来模拟请求体的重新发送:

class RequestContext(BaseHTTPMiddleware): 
    async def dispatch(self, request: Request, call_next: RequestResponseEndpoint): 
        request_id = request_ctx.set(str(uuid4())) # 为请求生成UUID
        
        # 读取原始请求体
        body = await request.body() 
        
        # 定义一个伪造的receive方法,返回已读取的body
        async def fake_receive():
            return {"type": "http.request", "body": body}
        
        # 替换request的receive方法
        original_receive = request._receive
        request._receive = fake_receive
        
        try:
            # 记录请求日志
            if body: 
                logger.info(f"Request [{request_id}] | Path: {request.url.path} | Body: {body.decode('utf-8')}")
            else: 
                logger.info(f"Request [{request_id}] | Path: {request.url.path} | No body")
            
            # 继续处理请求
            response = await call_next(request) 
            response.headers['X-Request-ID'] = request_ctx.get() 
            logger.info(f"Request [{request_id}] | Status: {response.status_code}") 
        finally:
            # 恢复原始的receive方法
            request._receive = original_receive
        
        request_ctx.reset(request_id) 
        return response

注意事项

  1. 编码问题:如果你的请求体不是UTF-8编码(比如表单提交的gbk编码数据),需要调整body.decode()的参数,避免乱码。如果不确定编码,也可以直接记录原始字节(但可读性差)。
  2. 大请求体:如果请求体特别大(比如上传文件),这种方式会把整个请求体加载到内存中,可能影响性能。这种场景下建议只记录请求元数据(比如路径、方法),而不是完整请求体。

内容的提问来源于stack exchange,提问作者sashaaero

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.09 10:52:40