mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-06 08:16:43 +00:00
fix(custom_httpx): resolve effective timeout instead of bypassing client defaults with None
AsyncHTTPHandler verb methods passed timeout=None straight to httpx when the
handler was built without one; httpx treats an explicit None as "no timeouts",
bypassing the client default the handler was created with, and Timeout errors
rendered the None literally ("Connection timed out after None seconds.").
Sync HTTPHandler verb methods already let the client default apply but printed
the same None in their error message.
- add _resolve_effective_timeout() mapping None to _DEFAULT_TIMEOUT
- async post/put/patch/delete now send the resolved timeout to httpx
- sync post/patch/put/delete error messages report the effective timeout
- explicit per-call timeouts are untouched and still win
Fixes #14635
This commit is contained in:
parent
75736323e6
commit
2f02e33db5
2 changed files with 83 additions and 8 deletions
|
|
@ -163,6 +163,17 @@ def _default_cached_client_timeout() -> httpx.Timeout:
|
|||
return httpx.Timeout(timeout=configured, connect=HTTP_HANDLER_CONNECT_TIMEOUT_SECONDS)
|
||||
|
||||
|
||||
def _resolve_effective_timeout(timeout: float | httpx.Timeout | None) -> float | httpx.Timeout:
|
||||
"""Resolve the timeout actually in effect for a request.
|
||||
|
||||
`None` means "use the client default", which `create_client` maps to
|
||||
`_DEFAULT_TIMEOUT`. An explicit `timeout=None` handed to httpx bypasses the
|
||||
client default entirely (no timeouts are enforced), so `None` must be
|
||||
resolved before building a request or rendering a Timeout error message.
|
||||
"""
|
||||
return timeout if timeout is not None else _DEFAULT_TIMEOUT
|
||||
|
||||
|
||||
_CLIENT_REFCOUNT_WHEN_HANDLER_IS_SOLE_REFERRER: Final = 2
|
||||
|
||||
|
||||
|
|
@ -687,7 +698,7 @@ class AsyncHTTPHandler:
|
|||
start_time: Final = time.time()
|
||||
try:
|
||||
if timeout is None:
|
||||
timeout = self.timeout
|
||||
timeout = _resolve_effective_timeout(self.timeout)
|
||||
|
||||
# Prepare data/content parameters to prevent httpx DeprecationWarning (memory leak fix)
|
||||
request_data, request_content = _prepare_request_data_and_content(data, content)
|
||||
|
|
@ -754,7 +765,7 @@ class AsyncHTTPHandler:
|
|||
):
|
||||
try:
|
||||
if timeout is None:
|
||||
timeout = self.timeout
|
||||
timeout = _resolve_effective_timeout(self.timeout)
|
||||
|
||||
# Prepare data/content parameters to prevent httpx DeprecationWarning (memory leak fix)
|
||||
request_data, request_content = _prepare_request_data_and_content(data, content)
|
||||
|
|
@ -818,7 +829,7 @@ class AsyncHTTPHandler:
|
|||
):
|
||||
try:
|
||||
if timeout is None:
|
||||
timeout = self.timeout
|
||||
timeout = _resolve_effective_timeout(self.timeout)
|
||||
|
||||
# Prepare data/content parameters to prevent httpx DeprecationWarning (memory leak fix)
|
||||
request_data, request_content = _prepare_request_data_and_content(data, content)
|
||||
|
|
@ -882,7 +893,7 @@ class AsyncHTTPHandler:
|
|||
):
|
||||
try:
|
||||
if timeout is None:
|
||||
timeout = self.timeout
|
||||
timeout = _resolve_effective_timeout(self.timeout)
|
||||
|
||||
# Prepare data/content parameters to prevent httpx DeprecationWarning (memory leak fix)
|
||||
request_data, request_content = _prepare_request_data_and_content(data, content)
|
||||
|
|
@ -1366,7 +1377,7 @@ class HTTPHandler:
|
|||
return response
|
||||
except httpx.TimeoutException:
|
||||
raise litellm.Timeout(
|
||||
message=f"Connection timed out after {timeout} seconds.",
|
||||
message=f"Connection timed out after {_resolve_effective_timeout(timeout)} seconds.",
|
||||
model="default-model-name",
|
||||
llm_provider="litellm-httpx-handler",
|
||||
)
|
||||
|
|
@ -1416,7 +1427,7 @@ class HTTPHandler:
|
|||
return response
|
||||
except httpx.TimeoutException:
|
||||
raise litellm.Timeout(
|
||||
message=f"Connection timed out after {timeout} seconds.",
|
||||
message=f"Connection timed out after {_resolve_effective_timeout(timeout)} seconds.",
|
||||
model="default-model-name",
|
||||
llm_provider="litellm-httpx-handler",
|
||||
)
|
||||
|
|
@ -1465,7 +1476,7 @@ class HTTPHandler:
|
|||
return response
|
||||
except httpx.TimeoutException:
|
||||
raise litellm.Timeout(
|
||||
message=f"Connection timed out after {timeout} seconds.",
|
||||
message=f"Connection timed out after {_resolve_effective_timeout(timeout)} seconds.",
|
||||
model="default-model-name",
|
||||
llm_provider="litellm-httpx-handler",
|
||||
)
|
||||
|
|
@ -1515,7 +1526,7 @@ class HTTPHandler:
|
|||
return response
|
||||
except httpx.TimeoutException:
|
||||
raise litellm.Timeout(
|
||||
message=f"Connection timed out after {timeout} seconds.",
|
||||
message=f"Connection timed out after {_resolve_effective_timeout(timeout)} seconds.",
|
||||
model="default-model-name",
|
||||
llm_provider="litellm-httpx-handler",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import pytest
|
|||
from aiohttp import ClientSession, TCPConnector
|
||||
|
||||
import litellm
|
||||
from litellm.constants import COMPLETION_HTTP_FALLBACK_SECONDS, HTTP_HANDLER_CONNECT_TIMEOUT_SECONDS
|
||||
from litellm.llms.custom_httpx.aiohttp_transport import LiteLLMAiohttpTransport
|
||||
from litellm.llms.custom_httpx.http_handler import (
|
||||
_CLIENT_REFCOUNT_WHEN_HANDLER_IS_SOLE_REFERRER,
|
||||
|
|
@ -1547,3 +1548,66 @@ def test_sync_force_ipv4_https_proxy_mount_uses_handler_ca_bundle(
|
|||
handler.close()
|
||||
|
||||
assert response.text == "ok-tls"
|
||||
class _TimeoutRecordingAsyncClient(httpx.AsyncClient):
|
||||
async def send(self, request, **kwargs):
|
||||
self.sent_timeout_extensions = request.extensions.get("timeout")
|
||||
raise httpx.ConnectTimeout("connect timed out", request=request)
|
||||
|
||||
|
||||
class _RecordingSyncClient(httpx.Client):
|
||||
def send(self, request, **kwargs):
|
||||
raise httpx.ReadTimeout("read timed out", request=request)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_post_without_timeout_enforces_default_and_reports_it():
|
||||
"""
|
||||
A handler built without a timeout used to pass timeout=None straight to
|
||||
httpx, which bypasses the client default entirely (no timeouts enforced)
|
||||
and produced "Connection timed out after None seconds." on failure.
|
||||
"""
|
||||
handler = AsyncHTTPHandler()
|
||||
assert handler.timeout is None
|
||||
client = _TimeoutRecordingAsyncClient()
|
||||
handler.client = client
|
||||
|
||||
with pytest.raises(litellm.Timeout) as exc_info:
|
||||
await handler.post("https://example.test/v1/chat", json={"ping": True})
|
||||
|
||||
assert client.sent_timeout_extensions["connect"] == HTTP_HANDLER_CONNECT_TIMEOUT_SECONDS
|
||||
assert client.sent_timeout_extensions["read"] == COMPLETION_HTTP_FALLBACK_SECONDS
|
||||
assert "None seconds" not in str(exc_info.value)
|
||||
assert f"{COMPLETION_HTTP_FALLBACK_SECONDS}" in str(exc_info.value)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_post_explicit_timeout_is_enforced_and_reported():
|
||||
handler = AsyncHTTPHandler()
|
||||
handler.client = _TimeoutRecordingAsyncClient()
|
||||
|
||||
with pytest.raises(litellm.Timeout) as exc_info:
|
||||
await handler.post("https://example.test/v1/chat", json={"ping": True}, timeout=3.5)
|
||||
|
||||
assert "None seconds" not in str(exc_info.value)
|
||||
assert "3.5" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_sync_post_timeout_message_reports_client_default_when_timeout_unset():
|
||||
handler = HTTPHandler()
|
||||
handler.client = _RecordingSyncClient()
|
||||
|
||||
with pytest.raises(litellm.Timeout) as exc_info:
|
||||
handler.post("https://example.test/v1/chat", json={"ping": True})
|
||||
|
||||
assert "None seconds" not in str(exc_info.value)
|
||||
assert f"{COMPLETION_HTTP_FALLBACK_SECONDS}" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_sync_post_timeout_message_reports_explicit_timeout():
|
||||
handler = HTTPHandler()
|
||||
handler.client = _RecordingSyncClient()
|
||||
|
||||
with pytest.raises(litellm.Timeout) as exc_info:
|
||||
handler.post("https://example.test/v1/chat", json={"ping": True}, timeout=3.5)
|
||||
|
||||
assert "3.5 seconds" in str(exc_info.value)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue