mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-14 09:03:16 +00:00
fix(custom_httpx): don't resend request on connection error when retries are disabled
AsyncHTTPHandler.post/put/patch/delete retried once on a fresh client whenever the first send raised ConnectError/RemoteProtocolError. That resend sits beneath the user-facing retry budget, so max_retries=0 still sent the request twice, which can double-bill non-idempotent requests. Gate the new-session resend on the configured retry budget: when retries are disabled (litellm.num_retries == 0) the original error propagates and the request is sent at most once. Default behavior is unchanged. Fixes #47824
This commit is contained in:
parent
9d8fe1ddb5
commit
8e53f3da59
2 changed files with 40 additions and 0 deletions
|
|
@ -278,6 +278,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
|
||||
# Key: tuple of (cafile, ssl_security_level, ssl_ecdh_curve)
|
||||
# Value: ssl.SSLContext
|
||||
|
|
@ -853,6 +857,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:
|
||||
|
|
@ -923,6 +929,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:
|
||||
|
|
@ -995,6 +1003,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:
|
||||
|
|
@ -1063,6 +1073,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:
|
||||
|
|
|
|||
|
|
@ -1546,6 +1546,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."""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue