From 625ed0ee8c683dd8a7d87564a14bdbd4472d2a44 Mon Sep 17 00:00:00 2001 From: Andy Young Date: Tue, 19 May 2026 17:04:11 -0700 Subject: [PATCH] fix(openai): guard httpx finalizers (#37570) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same shape as the merged anthropic patch in #37064, ported to `libs/partners/openai`. `_SyncHttpxClientWrapper.__del__` / `_AsyncHttpxClientWrapper.__del__` check `self.is_closed`, which reads `self._state`. When a wrapper is created without `__init__` running to completion — `copy.deepcopy` via `__new__` + `__setstate__`, or a constructor that raised partway through — `_state` is missing and the finalizer prints ``` Exception ignored in: Traceback (most recent call last): File ".../langchain_openai/chat_models/_client_utils.py", line 366, in __del__ if self.is_closed: File ".../httpx/_client.py", line 228, in is_closed return self._state == ClientState.CLOSED AttributeError: '_SyncHttpxClientWrapper' object has no attribute '_state' ``` at GC time. Same noise pattern that #37064 fixed for the anthropic partner. Hoist the `is_closed` access inside the existing `try/except` so the `AttributeError` is swallowed alongside the `close()` / `aclose()` exceptions that block already handles. Tests: two new unit tests build the wrappers via `__new__` (no `__init__` → no `_state`) and call `__del__` directly, mirroring the tests added in #37064. Verified: - `cd libs/partners/openai && make format` -> all checks passed - `cd libs/partners/openai && make test TEST_FILE=tests/unit_tests/chat_models/test_client_utils.py` -> 37 passed, 1 skipped (linux-only) - `cd libs/partners/openai && make lint` -> all checks passed, mypy clean --- .../chat_models/_client_utils.py | 10 ++++------ .../chat_models/test_client_utils.py | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/libs/partners/openai/langchain_openai/chat_models/_client_utils.py b/libs/partners/openai/langchain_openai/chat_models/_client_utils.py index 1d7acc6472d..d853d19d454 100644 --- a/libs/partners/openai/langchain_openai/chat_models/_client_utils.py +++ b/libs/partners/openai/langchain_openai/chat_models/_client_utils.py @@ -363,10 +363,9 @@ class _SyncHttpxClientWrapper(openai.DefaultHttpxClient): """Borrowed from openai._base_client.""" def __del__(self) -> None: - if self.is_closed: - return - try: + if self.is_closed: + return self.close() except Exception: # noqa: S110 pass @@ -376,10 +375,9 @@ class _AsyncHttpxClientWrapper(openai.DefaultAsyncHttpxClient): """Borrowed from openai._base_client.""" def __del__(self) -> None: - if self.is_closed: - return - try: + if self.is_closed: + return # TODO(someday): support non asyncio runtimes here asyncio.get_running_loop().create_task(self.aclose()) except Exception: # noqa: S110 diff --git a/libs/partners/openai/tests/unit_tests/chat_models/test_client_utils.py b/libs/partners/openai/tests/unit_tests/chat_models/test_client_utils.py index 37679c3a8ed..7605776d0fc 100644 --- a/libs/partners/openai/tests/unit_tests/chat_models/test_client_utils.py +++ b/libs/partners/openai/tests/unit_tests/chat_models/test_client_utils.py @@ -810,3 +810,21 @@ def test_client_build_applies_socket_options_when_user_opts_in( assert all(tuple(opts) == tuple(explicit) for opts in recorded), ( f"expected user-supplied opts, got {recorded!r}" ) + + +def test_sync_client_wrapper_del_handles_uninitialized_client() -> None: + """Test sync wrapper finalizer handles clients without initialized state.""" + client = _client_utils._SyncHttpxClientWrapper.__new__( + _client_utils._SyncHttpxClientWrapper + ) + + client.__del__() + + +async def test_async_client_wrapper_del_handles_uninitialized_client() -> None: + """Test async wrapper finalizer handles clients without initialized state.""" + client = _client_utils._AsyncHttpxClientWrapper.__new__( + _client_utils._AsyncHttpxClientWrapper + ) + + client.__del__()