Python aiohttp下载图片报ClientPayloadError的排查与重试方案
前置环境
- Python 3.9.5
- aiohttp 3.7.4.post0
问题场景
使用aiohttp异步下载图片时,99%场景运行正常,偶发执行data = await response.read()时抛出aiohttp.client_exceptions.ClientPayloadError: Response payload is not completed异常,同URL二次请求大概率恢复正常。
根据aiohttp官方文档,该异常为读取响应负载阶段触发,具体触发场景包含三类:
- 响应压缩无效
- 分块编码格式错误
- 实际返回数据量不匹配
Content-Length头声明的长度
解决方案
一、异常根因定位方法
首先排除比特位翻转的猜测:TCP协议自带校验和机制,传输过程中出现的比特损坏会被TCP层直接识别并触发重传,不会透传到应用层触发该异常,这类偶发、重试可恢复的错误基本都是网络传输、服务端或中间代理的问题,按以下步骤定位即可:
- 在异常捕获逻辑中打印全量上下文信息,包括响应头、已接收字节数、声明的Content-Length值、异常详情,参考代码:
import logging async def download_image(url): async with aiohttp.ClientSession() as session: async with session.get(url) as response: if response.status != 200: raise exceptions.FileNotFound() try: data = await response.read() except aiohttp.ClientPayloadError as e: logging.error( "Payload错误, 异常信息: %s, 响应头: %s, 已接收字节数: %s, 请求URL: %s", str(e), dict(response.headers), response.content.total_bytes, url ) raise img = Image.open(io.BytesIO(data)) return img
- 根据日志内容判断根因:
- 如果已接收字节数小于
Content-Length声明值,属于最常见场景,原因是网络波动、服务端主动断连、中间代理(CDN、Nginx等)超时提前截断连接,导致响应没传完就断开 - 如果异常信息提示压缩相关错误,检查
Content-Encoding响应头,多为服务端错误声明压缩格式、返回的压缩数据损坏 - 如果异常信息提示分块编码错误,基本是服务端或反向代理的分块传输实现bug,比如代理配置的响应超时过短,提前中断分块流
二、优化后的重试实现方案
外层直接捕获异常整函数重试的方案效率低,还存在频繁创建销毁Session的问题,按以下逻辑优化即可:
- 全局复用
ClientSession:Session本身内置连接池,每次请求新建Session会频繁销毁建立TCP连接,反而提升连接异常概率,全局初始化一次复用即可 - 配置明确的超时参数,避免僵死连接占用资源
- 重试仅覆盖传输层偶发异常,不对4xx、5xx这类确定性响应错误重试,避免无效请求
- 重试加指数退避间隔,避免瞬时压力打满服务端
参考实现代码:
import asyncio import io import aiohttp from PIL import Image # 全局Session单例 _SESSION = None async def _get_session(): global _SESSION if _SESSION is None: # 配置全链路超时:总超时30s,连接超时10s,socket读取超时10s timeout = aiohttp.ClientTimeout(total=30, connect=10, sock_read=10) _SESSION = aiohttp.ClientSession(timeout=timeout) return _SESSION async def download_image(url, max_retry=3): session = await _get_session() for retry_idx in range(max_retry): try: async with session.get(url) as resp: if resp.status != 200: raise exceptions.FileNotFound() data = await resp.read() return Image.open(io.BytesIO(data)) # 仅重试传输层偶发异常 except (aiohttp.ClientPayloadError, aiohttp.ClientConnectionError, asyncio.TimeoutError): if retry_idx == max_retry - 1: # 最后一次重试失败直接抛出 raise # 指数退避等待:0.5s、1s、2s await asyncio.sleep(0.5 * (2 ** retry_idx))
该方案相比外层整函数重试,连接复用率更高,重试逻辑更精准,不会产生无效请求,对服务端的压力也更小。
内容的提问来源于stack exchange,提问作者user15423800
相关产品推荐
相关产品推荐

