现象
动作生成任务在视频已经生成完毕之后失败,整单丢弃,费用照付。
2026-08-05 实测,同一角色连续两单都死在同一处,各烧掉一次生成费用:
peer closed connection without sending complete message body
(received 720450 bytes, expected 929531)
第三单一次成功——两次复现对一次成功,说明这不是稳定失败,是概率性的连接中断。
根因
取视频成品那一步是单次读取、无重试、不校验长度:
return client.get(url).raise_for_status().content
代码位置:backend/packages/framework/src/windup_framework/providers/sufy.py
(SufyVideoProvider.i2v 的最后一行;轮询到 completed、拿到 task_result.videos[0].url 之后)。
这一步的位置很特殊:它在"钱已经花完"之后。前面的提交任务、轮询、等待都成功了,
视频在供应商那边已经生成好,只差把 bytes 取回来。此时读 body 断一次,整单就废,
用户看到的是任务 FAILED,而账单上那次生成照常计费。
两个具体缺陷:
- 不重试。 这是对成品 URL 的 GET,幂等、不再计费。重试的代价是一次重下(几百 KB),
不重试的代价是一次重新生成。代价不对等。
- 不校验长度。 截断不一定抛异常——服务端提前关流而客户端已收到部分 body 时,
.content 可能直接返回短 bytes。那样坏视频会一路流到出帧环节才暴露,
在那里表现为"解码失败",很难回溯到下载这一步。
修复
_download(client, url, tries=3):指数退避重试 3 次;有 Content-Length 时校验实收字节数,
不符按失败重试;分块传输无该头时跳过校验;三次都失败抛 RuntimeError 并带出最后一次的真实原因。
回归测试用 httpx.MockTransport,不联网。已用旧实现做控制样本验证:
三条断言(重试成功 / 拒绝截断 / 包装最终原因)在修复前确实失败,两条护栏断言两边都过。
影响面与不在本次范围内的同类点
同一份代码里还有两处 resp.raise_for_status(); return resp.content
(server/orchestrator/executor.py 的 _download_master 与 _download),
但它们取的是母版 / 参考图,即输入,失败发生在付费生成之前,损失只有一次任务而没有账单。
严重性不同一档,故不并进本 PR;如需一并加固可另开。
SufyImageProvider.gen_image 走 .json() 取内联 base64 图,截断会抛 JSONDecodeError
而不是静默短读,同样在付费之后,但目前没有实测复现,暂不动。
验收
- 首次读 body 断连、第二次成功时,任务应完成而不是 FAILED;
- 服务端声明长度与实收不符时,不得把短 bytes 交给下游;
- 三次都失败时报错信息要带出最后一次的网络层原因,而不是笼统的"生成失败"。
现象
动作生成任务在视频已经生成完毕之后失败,整单丢弃,费用照付。
2026-08-05 实测,同一角色连续两单都死在同一处,各烧掉一次生成费用:
第三单一次成功——两次复现对一次成功,说明这不是稳定失败,是概率性的连接中断。
根因
取视频成品那一步是单次读取、无重试、不校验长度:
代码位置:
backend/packages/framework/src/windup_framework/providers/sufy.py(
SufyVideoProvider.i2v的最后一行;轮询到completed、拿到task_result.videos[0].url之后)。这一步的位置很特殊:它在"钱已经花完"之后。前面的提交任务、轮询、等待都成功了,
视频在供应商那边已经生成好,只差把 bytes 取回来。此时读 body 断一次,整单就废,
用户看到的是任务 FAILED,而账单上那次生成照常计费。
两个具体缺陷:
不重试的代价是一次重新生成。代价不对等。
.content可能直接返回短 bytes。那样坏视频会一路流到出帧环节才暴露,在那里表现为"解码失败",很难回溯到下载这一步。
修复
_download(client, url, tries=3):指数退避重试 3 次;有Content-Length时校验实收字节数,不符按失败重试;分块传输无该头时跳过校验;三次都失败抛
RuntimeError并带出最后一次的真实原因。回归测试用
httpx.MockTransport,不联网。已用旧实现做控制样本验证:三条断言(重试成功 / 拒绝截断 / 包装最终原因)在修复前确实失败,两条护栏断言两边都过。
影响面与不在本次范围内的同类点
同一份代码里还有两处
resp.raise_for_status(); return resp.content(
server/orchestrator/executor.py的_download_master与_download),但它们取的是母版 / 参考图,即输入,失败发生在付费生成之前,损失只有一次任务而没有账单。
严重性不同一档,故不并进本 PR;如需一并加固可另开。
SufyImageProvider.gen_image走.json()取内联 base64 图,截断会抛 JSONDecodeError而不是静默短读,同样在付费之后,但目前没有实测复现,暂不动。
验收