bigywater.tech
爬虫工程 · 14

连接异常:RemoteDisconnected、ConnectionReset 分别由什么触发

爬虫日志里最常见的错误是一长串“Connection aborted”“Max retries exceeded”。这篇用我写的套接字服务器逐种制造:对端礼貌地关、直接重置、回了乱码、响应头太多、半截响应,看 requests 到底抛什么,最里层的原因是什么,又会不会被 except ConnectionError 接住。

FIN / RST
对端礼貌关闭是 RemoteDisconnected,被重置是 ConnectionResetError
200 + 空
只发半截响应头就断开时,requests 不报错,返回 200、空响应头、空内容
否 / 否
响应体阶段的 ChunkedEncodingError 既不是 ConnectionError 也不是 Timeout

服务器行为 → requests 异常

实测 · 本机执行输出
  没有任何服务在监听(端口被拒绝)
      requests 抛出:ConnectionError;内部链:ConnectionError → MaxRetryError → NewConnectionError → ConnectionRefusedError;最内层:[Errno 61] Connection refused;ConnectionError/Timeout = 是/否
  收到请求后,读完并正常关闭连接(FIN)
      requests 抛出:ConnectionError;内部链:ConnectionError → ProtocolError → RemoteDisconnected;最内层:Remote end closed connection without response;ConnectionError/Timeout = 是/否
  请求字节到达后,不读直接关闭(缓冲区有未读数据,系统发 RST)
      requests 抛出:ConnectionError;内部链:ConnectionError → ProtocolError → ConnectionResetError;最内层:[Errno 54] Connection reset by peer;ConnectionError/Timeout = 是/否
  读完请求后,设 SO_LINGER=0 强制关闭(RST)
      requests 抛出:ConnectionError;内部链:ConnectionError → ProtocolError → ConnectionResetError;最内层:[Errno 54] Connection reset by peer;ConnectionError/Timeout = 是/否
  只发了半截响应头就关闭
      requests 抛出:(无异常);内部链:状态 200;最内层:;ConnectionError/Timeout = 
  回了不是 HTTP 的内容
      requests 抛出:ConnectionError;内部链:ConnectionError → ProtocolError → BadStatusLine;最内层:hello world\r\n;ConnectionError/Timeout = 是/否
  状态行里状态码不是数字
      requests 抛出:ConnectionError;内部链:ConnectionError → ProtocolError → BadStatusLine → ValueError;最内层:invalid literal for int() with base 10: 'abc';ConnectionError/Timeout = 是/否
  一条响应头长度 70000 字节
      requests 抛出:ConnectionError;内部链:ConnectionError → ProtocolError → LineTooLong;最内层:got more than 65536 bytes when reading header line;ConnectionError/Timeout = 是/否
  响应头 150 个
      requests 抛出:ConnectionError;内部链:ConnectionError → ProtocolError → HTTPException;最内层:got more than 100 headers;ConnectionError/Timeout = 是/否
  Content-Length: 1000 只发 100 字节就关闭
      requests 抛出:ChunkedEncodingError;内部链:ChunkedEncodingError → ProtocolError → IncompleteRead;最内层:IncompleteRead(100 bytes read, 900 more expected);ConnectionError/Timeout = 否/否
  Content-Length: 1000 发 100 字节后发 RST
      requests 抛出:ChunkedEncodingError;内部链:ChunkedEncodingError → ProtocolError → ConnectionResetError;最内层:[Errno 54] Connection reset by peer;ConnectionError/Timeout = 否/否
  (细看"只发了半截响应头就关闭":requests 没有报错,r.status_code=200,收到的响应头={},r.content=b'')

连接与响应头阶段的异常对外都是 requests.ConnectionError,要看内部链最里层才知道原因:没人监听是 ConnectionRefusedError;服务器读完请求后正常关闭(FIN)是 RemoteDisconnected;关闭时缓冲区里有未读数据或设了 SO_LINGER=0(RST)是 ConnectionResetError。第 4 篇里的竞态就是 RemoteDisconnected。

响应不合规时:不是 HTTP 是 BadStatusLine;一条响应头超过 65536 字节是 LineTooLong;响应头超过 100 个是 HTTPException。响应体读到一半断开(长度不足或被重置)是 ChunkedEncodingError。

最值得注意的是半截响应头:服务器发了 HTTP/1.1 200 OK\r\nContent-Le 就关闭,requests 返回 200、空头、空内容,没有任何异常。爬虫如果只看状态码,就会把这种半截响应当成功。

协议不匹配

实测 · 本机执行输出
  https:// 去连明文 HTTP 服务器:抛 SSLError;内部链 SSLError → MaxRetryError → SSLError → SSLError;最内层 [SSL: WRONG_VERSION_NUMBER] wrong version number (_ssl.c:1082);ConnectionError/Timeout = 是/否
  http:// 去连一个 TLS 服务器:抛 ConnectionError;内部链 ConnectionError → ProtocolError → ConnectionResetError;最内层 [Errno 54] Connection reset by peer;ConnectionError/Timeout = 是/否
  https:// 连自签名证书的服务器且 verify=True:抛 SSLError;内部链 SSLError → MaxRetryError → SSLError → SSLCertVerificationError;最内层 [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certific;ConnectionError/Timeout = 是/否

https 地址连到明文端口得到 SSLError(WRONG_VERSION_NUMBER);http 地址连到 TLS 端口,服务器看到的是乱码,这里得到 ConnectionResetError;自签名证书加 verify=True 是 SSLError(CERTIFICATE_VERIFY_FAILED)。这三种都是 ConnectionError 的子类。

哪个 except 能抓到

实测 · 本机执行输出
  ConnectionError:ConnectionError → RequestException → OSError → Exception
  ConnectTimeout:ConnectTimeout → ConnectionError → Timeout → RequestException → OSError → Exception
  ReadTimeout:ReadTimeout → Timeout → RequestException → OSError → Exception
  SSLError:SSLError → ConnectionError → RequestException → OSError → Exception
  ProxyError:ProxyError → ConnectionError → RequestException → OSError → Exception
  ChunkedEncodingError:ChunkedEncodingError → RequestException → OSError → Exception
  ContentDecodingError:ContentDecodingError → RequestException → OSError → HTTPError → Exception
  TooManyRedirects:TooManyRedirects → RequestException → OSError → Exception
  InvalidURL:InvalidURL → RequestException → OSError → ValueError → Exception

ConnectTimeout、SSLError、ProxyError 继承自 ConnectionError;ReadTimeout 只继承 Timeout;ChunkedEncodingError 既不是 ConnectionError 也不是 Timeout。所以只写 except ConnectionError 会漏掉读超时和响应体阶段的断开;想统一处理要捕获 requests.RequestException。

分步演示

连接异常
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / requests 2.34.2 / aiohttp 3.14.3 / lxml 6.1.1(系统 Python);所有请求只发给本机自己起的测试服务器(127.0.0.1,端口 9960 以上,不访问任何真实网站);服务器可以按实验需要制造延迟、断连、压缩、重定向等行为,并记录收到的原始请求;计时用 time.perf_counter,数字每次运行会有小幅抖动。服务器是 RawSrv:收到请求后按场景直接用套接字发送字节、关闭或用 SO_LINGER=0 重置;异常链通过沿着异常的 args、reason、__cause__ 逐层展开得到;“ConnectionError / Timeout”是对抛出的异常做 isinstance 判断
    • 没有验证:Linux、Windows 上同样场景下的异常类(macOS 上 RST 对应 Errno 54,Linux 是 Errno 104);带重试(Retry)时的行为;aiohttp 和 httpx 的对应异常(aiohttp 的 ServerDisconnectedError、ClientPayloadError 等没有测);半截响应头“成功”这一现象在其它 Python/urllib3 版本里是否同样出现;HTTP/2 下的连接异常;代理场景下的连接异常
    京ICP备2026067502号-1京公网安备11010502063487号