aiohttp 读取空 multipart 分段,为什么报 Chunk size 断言
aiohttp 3.14.3 把 Content-Length: 0 当成未知长度,短块读取误触 boundary 长度断言。限定 mixed/related 分段,使用 3.14.4 已发布修复并验证后续 part 仍可读取。
multipart 响应里有一个空分段,调用 part.read_chunk(4) 时却出现:
AssertionError: Chunk size must be greater or equal than boundary length + 2
如果使用 aiohttp 3.14.3,分段属于 multipart/mixed 或 multipart/related,而且它自己的头部明确写了 Content-Length: 0,可能遇到已知的零长度判断问题。这里的长度是单个 part 的长度,不是整个 HTTP 响应的长度。
零字节为何会走进 boundary 检查
3.14.3 的 BodyPartReader.read_chunk() 用 if self._length: 选择读取策略。正数进入已知长度分支,None 进入扫描 boundary 的分支,但整数 0 也被判为假,于是合法的空分段被当成了未知长度。
扫描分支要求块大小至少达到 boundary 长度加 2,才能识别边界。此时传入 4 等较小值,会在真正返回空内容之前触发断言。这个现象不能直接证明发送方的 boundary 已经损坏,也不说明空文件必须填充一些字节才能上传。
multipart/form-data 有另外的处理:aiohttp 不按同样方式使用其 part 内的 Content-Length。不能只看到一个表单空文件就把本问题套上去;先确认外层 multipart 类型、实际 part 头部、aiohttp 版本和报错路径。
使用已正确区分 0 与 None 的版本
aiohttp 3.14.4 已把条件改成 self._length is not None,3.14.5 也保留该实现。这样 0 会进入已知长度分支,并把分段标记为结束。升级时仍需核对上层依赖约束、锁文件和运行环境;这两个版本要求 Python 3.10 或更高。
下面是已有 BodyPartReader 的消费方式示意。part 应来自项目自己的 MultipartReader.next(),不需要为了读取空分段而访问其私有 _length:
chunk = await part.read_chunk(4)
if chunk == b"":
# 当前分段读完,按原业务流程处理空内容
pass
4 字节用于覆盖这个特定回归场景,不是推荐给所有 multipart 读取的块大小。对没有已知长度的其他 part,boundary 的最小块大小要求仍然存在,应使用足够大的块或默认 read_chunk(),不能把升级理解为任意分段都能逐字节读取。
短期无法升级时,使用默认块大小或确保大小满足 boundary 要求,可以绕开这个断言触发条件,但没有修正 3.14.3 的策略判断。不要使用 python -O 关闭断言,也不要把所有 AssertionError 转成空分段;那会隐藏非空数据或损坏消息的真正错误。
空内容之外,还要检查边界是否留给了下一个分段
验收包含显式零长度的 mixed/related 分段、正常非空分段,以及后面紧接另一个 part 的消息。空分段应返回 b"",下一个分段的头部与内容仍应完整,最终结束边界也应正确消费。
对报文中宣称长度为 0、实际却塞入数据的情况,仍应按格式错误处理,不能把这些数据悄悄丢掉。增加块大小后“不再断言”不足以证明解析结果正确,恢复部署时应保留原始样本和旧锁文件,避免排查时丢失具体触发条件。


