用 requests 2.32 的 Session 加 urllib3 Retry 发流式上传,第一次连接超时后重试直接抛 ValueError: I/O operation on closed file,因为 body 是打开的文件对象,重试时已经读到末尾,服务端只收到半个文件。

一开始怀疑是「没有用 bytes 或可 rewind 的 body」,查了一圈,方向不对。

排查方向其实有限,先把可能性排一排。文件对象只能读一次,重试时指针在末尾。少数情况下是Retry 没排除 read 超时,重试了不该重试的请求。

不容易想到的是,urllib3 在 adapter 层重试,业务层没有感知。

复现同一个文件对象被读两次

python -c "f=open('a.bin','rb');print(len(f.read()));print(len(f.read()))"

接着在重试前把文件指针复位

python -c "f=open('a.bin','rb');f.seek(0);print(f.tell())"

然后改用 bytes 或自定义可重放的 body

python -c "print(len(open('a.bin','rb').read()))"

只对连接错误重试并限制次数

python -c "from urllib3.util.retry import Retry;print(Retry(total=2,connect=2,read=0,status=0))"

改完先别撤,确认这几项。重试后服务端收到的文件大小与源文件一致,日志里没有 I/O operation on closed file,校验和相同。

提醒一点。requests 的重试在 adapter 层,body 不会自动重放;上传大文件时要么读成 bytes,要么每次重试重新打开文件,别把打开的文件对象直接交给 Retry。这类问题最好一次改到位,反复小改反而容易把原因搅混。

作者 9823189