This commit is contained in:
Azmi Muwahid 2026-11-05 07:17:27 +00:00 • committed by GitHub
commit b9bb611f8e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 40 additions and 0 deletions

View file

@ -312,6 +312,10 @@ def _prepare_request_data_and_content(
return request_data, request_content
def _connection_error_resend_allowed() -> bool:
return litellm.num_retries is None or litellm.num_retries > 0
# Cache for SSL contexts to avoid creating duplicate contexts with the same configuration
_ssl_context_cache: Final[dict[tuple[str | None, str | None, str | None, bool], ssl.SSLContext]] = {}
@ -969,6 +973,8 @@ class AsyncHTTPHandler:
response.raise_for_status()
return response
except httpx.ConnectError:
if not _connection_error_resend_allowed():
raise
# Retry the request with a new session if there is a connection error
new_client: Final = self.create_client(timeout=timeout, event_hooks=self.event_hooks)
try:
@ -1027,6 +1033,8 @@ class AsyncHTTPHandler:
response.raise_for_status()
return response
except (httpx.RemoteProtocolError, httpx.ConnectError):
if not _connection_error_resend_allowed():
raise
# Retry the request with a new session if there is a connection error
new_client: Final = self.create_client(timeout=timeout, event_hooks=self.event_hooks)
try:
@ -1089,6 +1097,8 @@ class AsyncHTTPHandler:
response.raise_for_status()
return response
except httpx.ConnectError:
if not _connection_error_resend_allowed():
raise
# Retry the request with a new session if there is a connection error
new_client: Final = self.create_client(timeout=timeout, event_hooks=self.event_hooks)
try:
@ -1146,6 +1156,8 @@ class AsyncHTTPHandler:
response.raise_for_status()
return response
except (httpx.RemoteProtocolError, httpx.ConnectError):
if not _connection_error_resend_allowed():
raise
# Retry the request with a new session if there is a connection error
new_client: Final = self.create_client(timeout=timeout, event_hooks=self.event_hooks)
try:

View file

@ -1601,6 +1601,34 @@ async def test_idempotent_disconnect_is_replayed(method: str) -> None:
await handler.close()
@pytest.mark.asyncio
@pytest.mark.parametrize("method", ["post", "put", "patch", "delete"])
async def test_connection_error_not_resent_when_retries_disabled(method: str, monkeypatch) -> None:
first_attempts: Final[list[httpx.Request]] = [] # mutable-ok: async closure capture buffer
resent: Final[list[httpx.Request]] = [] # mutable-ok: async closure capture buffer
async def raise_connection_error(request: httpx.Request) -> httpx.Response:
first_attempts.append(request)
raise httpx.ConnectError("connection reset after the request was written", request=request)
async def capture_resend(request: httpx.Request) -> httpx.Response:
resent.append(request)
return httpx.Response(200, request=request)
first: Final = httpx.AsyncClient(transport=httpx.MockTransport(raise_connection_error))
retry: Final = httpx.AsyncClient(transport=httpx.MockTransport(capture_resend))
monkeypatch.setattr(litellm, "num_retries", 0)
async with first, retry:
handler: Final = _RetryClientHandler(first=first, retry=retry)
with pytest.raises(httpx.ConnectError):
await getattr(handler, method)("https://api.example.com/runs/batch", content=b'{"a":1}')
assert len(first_attempts) == 1
assert resent == []
await handler.close()
@pytest.fixture
def forward_proxy_server():
"""Plain HTTP forward proxy that records the absolute URIs it is asked to fetch."""