From 94df3e30e49e8ebd5c8895ad700296183fb8f0f1 Mon Sep 17 00:00:00 2001 From: Harish Date: Thu, 9 Apr 2026 17:59:31 -0700 Subject: [PATCH] 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. --- litellm/llms/custom_httpx/http_handler.py | 50 +++++++++++++++++++ .../llms/custom_httpx/test_http_handler.py | 42 +++++++++++++++- 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/litellm/llms/custom_httpx/http_handler.py b/litellm/llms/custom_httpx/http_handler.py index 001547557d4..bed34e9ecf0 100644 --- a/litellm/llms/custom_httpx/http_handler.py +++ b/litellm/llms/custom_httpx/http_handler.py @@ -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( diff --git a/tests/test_litellm/llms/custom_httpx/test_http_handler.py b/tests/test_litellm/llms/custom_httpx/test_http_handler.py index 0a3f0fe5e67..d18f25231ff 100644 --- a/tests/test_litellm/llms/custom_httpx/test_http_handler.py +++ b/tests/test_litellm/llms/custom_httpx/test_http_handler.py @@ -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()