Skip to content

fix(client): guard against AttributeError in AsyncHttpxClientWrapper.…#2963

Open
raulblazquezbullon wants to merge 1 commit intoopenai:mainfrom
raulblazquezbullon:fix/async-client-del-attribute-error
Open

fix(client): guard against AttributeError in AsyncHttpxClientWrapper.…#2963
raulblazquezbullon wants to merge 1 commit intoopenai:mainfrom
raulblazquezbullon:fix/async-client-del-attribute-error

Conversation

@raulblazquezbullon
Copy link

@raulblazquezbullon raulblazquezbullon commented Mar 12, 2026

  • I understand that this repository is auto-generated and my pull request may not be merged

Changes being requested

Move the is_closed guard inside the try/except block in AsyncHttpxClientWrapper.__del__.

When copy.deepcopy() is called on an object that holds an AsyncOpenAI instance (e.g. via dataclasses.asdict() on a dataclass containing an OpenAIProvider field), Python's deepcopy machinery creates a new AsyncHttpxClientWrapper via __new__(), leaving __dict__ completely empty — and then fails while trying to copy the internal asyncio transport and locks. The partially constructed object is released, the GC collects it, and __del__ crashes because _state was never set.

Observed traceback:

Exception ignored in: <function AsyncHttpxClientWrapper.__del__ at 0x106384ae0>
Traceback (most recent call last):
  File ".../openai/_base_client.py", line 1430, in __del__
    if self.is_closed:
       ^^^^^^^^^^^^^^
  File ".../httpx/_client.py", line 202, in is_closed
    return self._state == ClientState.CLOSED
           ^^^^^^^^^^^
AttributeError: 'AsyncHttpxClientWrapper' object has no attribute '_state'

Concrete trigger (confirmed via debugger):

MLflow's agent tracer patches Runner.run() and calls dataclasses.asdict(run_config) to set span attributes. asdict() calls copy.deepcopy() on non-dataclass fields, reaching OpenAIProvider -> AsyncOpenAI -> AsyncHttpxClientWrapper. deepcopy creates the wrapper via __new__() (empty __dict__), then raises an error copying asyncio internals. The failed copy gets GC'd with no _state.

The fix moves the check inside the existing try/except Exception, consistent with the method's original intent to silence all finalization errors. No functional behaviour changes for fully initialized objects.

Additional context & links

  • The object has __dict__ = {} at __del__ time, confirming init was never called, not that it failed partway through.
  • The root cause is also a bug in the caller (dataclasses.asdict should not deepcopy objects containing asyncio internals), but __del__ should be robust regardless of how the object was constructed.
  • The fix is purely defensive and introduces no new public API surface.

@raulblazquezbullon raulblazquezbullon requested a review from a team as a code owner March 12, 2026 17:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant