Http connection reset (#25434)

* Possible fix for connection reset happening during read(), since payload sizes are large there can be a connection reset mid read.

Goal: Reproduce this minimally with aiohttp and write test cases

* Add debug logs and manually capture 104 connection reset errors, and retry. This is a id fix and doesnt fix root cause.

Root cause could be due to firewall or proxy resets or connection resets due as rate limiting.

* Simplify exception handling in HTTP request methods

Removed ReadError from exception handling in multiple request methods.
This commit is contained in:
Harish 2026-04-09 17:59:31 -07:00 • committed by GitHub
parent e7551a1d43
commit 94df3e30e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 91 additions and 1 deletions

View file

@ -113,6 +113,23 @@ def _prepare_request_data_and_content(
return request_data, request_content
def _is_connection_reset_error(e: Exception) -> bool:
error_text = getattr(e, "text", str(e))
error_response = getattr(e, "response", None)
if error_response and hasattr(error_response, "text"):
error_text = getattr(error_response, "text", error_text)
if isinstance(error_text, bytes):
error_text = error_text.decode("utf-8", errors="ignore")
normalized_error_text = str(error_text).lower()
return (
"errno 104" in normalized_error_text
or "connection reset by peer" in normalized_error_text
or "connection reset" in normalized_error_text
)
# Cache for SSL contexts to avoid creating duplicate contexts with the same configuration
# Key: tuple of (cafile, ssl_security_level, ssl_ecdh_curve)
# Value: ssl.SSLContext
@ -482,7 +499,14 @@ class AsyncHTTPHandler:
params=params,
headers=headers,
stream=stream,
content=content,
)
except Exception as retry_error:
verbose_logger.debug(
"httpx.ConnectError retry failed in AsyncHTTPHandler.post(): %s",
str(retry_error),
)
raise
finally:
await new_client.aclose()
except httpx.TimeoutException as e:
@ -512,6 +536,32 @@ class AsyncHTTPHandler:
raise e
except Exception as e:
if _is_connection_reset_error(e):
verbose_logger.debug(
"Detected connection reset error in AsyncHTTPHandler.post(); retrying with a new client"
)
new_client = self.create_client(
timeout=timeout, event_hooks=self.event_hooks
)
try:
return await self.single_connection_post_request(
url=url,
client=new_client,
data=data,
json=json,
params=params,
headers=headers,
stream=stream,
content=content,
)
except Exception as retry_error:
verbose_logger.debug(
"Connection reset retry failed in AsyncHTTPHandler.post(): %s",
str(retry_error),
)
raise
finally:
await new_client.aclose()
raise e
async def put(

View file

@ -3,7 +3,7 @@ import os
import pathlib
import ssl
import sys
from unittest.mock import MagicMock, patch
from unittest.mock import AsyncMock, MagicMock, patch
import certifi
import httpx
@ -658,3 +658,43 @@ async def test_httpx_handler_uses_env_user_agent(monkeypatch):
assert req.headers.get("User-Agent") == "Claude Code"
finally:
await handler.close()
@pytest.mark.asyncio
async def test_async_http_handler_retries_on_connection_reset_error_text():
handler = AsyncHTTPHandler()
retry_client = MagicMock()
retry_client.aclose = AsyncMock()
retry_response = MagicMock(spec=httpx.Response)
handler.client.send = AsyncMock(
side_effect=Exception("[Errno 104] Connection reset by peer")
)
handler.create_client = MagicMock(return_value=retry_client)
handler.single_connection_post_request = AsyncMock(return_value=retry_response)
try:
response = await handler.post(
url="https://example.com",
data={"hello": "world"},
headers={"x-test": "1"},
content="payload",
)
assert response is retry_response
handler.create_client.assert_called_once_with(
timeout=handler.timeout, event_hooks=handler.event_hooks
)
handler.single_connection_post_request.assert_awaited_once_with(
url="https://example.com",
client=retry_client,
data={"hello": "world"},
json=None,
params=None,
headers={"x-test": "1"},
stream=False,
content="payload",
)
retry_client.aclose.assert_awaited_once()
finally:
await handler.close()