litellm/tests/test_litellm/llms/custom_httpx
Maximilian Roos 3f00e6fa46
fix(http_handler): don't close the httpx client when a handler is collected
`AsyncHTTPHandler` and `HTTPHandler` each closed their httpx client from
`__del__`. Closing an httpx client tears down the connection pool every
in-flight response is streaming through, and it permanently invalidates the
client for later requests. Neither is safe on garbage collection, because a
handler is routinely collected while its client is still in use:

- Streaming. Nothing in a response's reference graph points back at the
  handler: httpx binds the response to the transport stream and the pool, never
  to the wrapper that built them. A cached handler therefore becomes
  collectable mid-stream when its one-hour TTL expires, and every body still
  being read off that pool is cut. That is #24929; the window recurs hourly per
  process, because the TTL clock starts at first client construction.

- Borrowed clients. `litellm/a2a_protocol/main.py` and
  `litellm/proxy/pass_through_endpoints/pass_through_endpoints.py` keep only
  `handler.client`. Both take the handler from `get_async_httpx_client`, so the
  cache pins it for the same one-hour TTL and then lets it go on eviction, while
  the borrowed client is still serving a longer-lived consumer:
  `create_a2a_client` hands its client to the a2a SDK and documents it as
  "create client once, reuse for multiple requests". The next request on that
  client raises `RuntimeError: Cannot send a request, as the client has been
  closed.`

- Someone else's client. `litellm/llms/azure/azure.py` wraps
  `litellm.client_session` in a throwaway `HTTPHandler` for a single image
  generation, and `HTTPHandler.close()` closes whatever client it was handed.
  One such call left the user's shared session closed for the rest of the
  process.

`LLMClientCache` already documents the invariant these finalizers broke:
evicted clients "may still be in use by in-flight requests", so the cache
deliberately does not close them and leaves them to normal garbage collection.
Closing on collection is precisely what that rules out.

Deleting both finalizers removes the cause instead of narrowing the window.
The async one was never dependable regardless: it needs a running event loop,
so collection outside one silently did nothing. Explicit
`close()`/`aclose()` is untouched, and `close_litellm_async_clients()` still
closes cached async clients at exit.

The cost is that an evicted client is no longer closed eagerly. On the default
aiohttp transport that shows up as five lines of asyncio ERROR output per
evicted handler ("Unclosed client session", "Unclosed connector", and their
context keys) with no change in descriptors or RSS, since aiohttp's own
connector finalizer closes the connections as it warns. On the httpcore
transport there is no log output, but idle keep-alive sockets from evicted
pools are held until a generation-2 collection reclaims them: over 5000
evictions that plateaus at 56 descriptors and ~1 MB of RSS, and with 20
concurrent requests per pool it peaks around 650 descriptors before each gen-2
pass returns it to baseline. Bounded by the GC cycle, not by the eviction
count.

Fixes #24929
2026-07-28 09:18:51 -07:00
..
test_aiohttp_cleanup_closed.py style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
test_aiohttp_handler.py style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
test_aiohttp_so_keepalive.py fix: added keepalive args for aiohttp tcpconnector 2026-04-29 09:14:57 -07:00
test_aiohttp_transport.py fix(streaming): surface upstream connection resets instead of empty 200 streams 2026-07-14 07:21:45 -07:00
test_credential_leak_prevention.py Litellm agent oss staging 05 11 2026 (#27733) 2026-05-13 14:09:12 -07:00
test_gemini_session_leak.py Fix __main__ exit bug, close litellm_async_client, restore global state 2026-03-14 18:14:42 -07:00
test_handler_gc_does_not_close_client.py fix(http_handler): don't close the httpx client when a handler is collected 2026-07-28 09:18:51 -07:00
test_http_handler.py Revert "fix(http_handler): keep a handler alive while its responses are still streaming" 2026-07-28 09:18:51 -07:00
test_llm_http_handler.py fix(anthropic): honor messages request timeout (#33418) 2026-07-15 14:37:48 -07:00
test_mock_transport.py style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00