Skip to content

视频已生成、费用已产生,却因下载读 body 断一次就整单丢弃(无重试 / 不校验长度) #129

Description

@johnnyzhang-eng

现象

动作生成任务在视频已经生成完毕之后失败,整单丢弃,费用照付。

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,而账单上那次生成照常计费。

两个具体缺陷:

  1. 不重试。 这是对成品 URL 的 GET,幂等、不再计费。重试的代价是一次重下(几百 KB),
    不重试的代价是一次重新生成。代价不对等。
  2. 不校验长度。 截断不一定抛异常——服务端提前关流而客户端已收到部分 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 交给下游;
  • 三次都失败时报错信息要带出最后一次的网络层原因,而不是笼统的"生成失败"。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

P1优先级 P1(次级)bugSomething isn't working

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions