fix(openai): guard httpx finalizers (#37570)

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: <function _SyncHttpxClientWrapper.__del__ at 0x...>
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
This commit is contained in:
Andy Young
2026-05-19 17:04:11 -07:00
committed by GitHub
parent b2ee0a9bba
commit 625ed0ee8c
2 changed files with 22 additions and 6 deletions

View File

@@ -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

View File

@@ -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__()