Skip to content

AttributeError in BaseApiClient GC cleanup when initialization fails #2012

@chidifrank

Description

@chidifrank

If BaseApiClient fails initialisation (e.g. due to conflicting arguments like project and api_key), the _http_options attribute is never set. When the GC later cleans up this partially initialised object, del schedules aclose(), which crashes when trying to access the missing _http_options.

Environment details

  • Programming language: Python
  • OS: macOS
  • Language runtime version: 3.12.0
  • Package version: 1.59.0

Steps to reproduce

import asyncio
import gc
from google.genai._api_client import BaseApiClient

async def main():
    try:
        # trigger ValueError early in __init__
        BaseApiClient(project="A", api_key="B") 
    except ValueError:
        pass
        
    # force GC to trigger __del__ and the subsequent crash
    gc.collect()

if __name__ == "__main__":
    asyncio.run(main())

>> Task exception was never retrieved
future: <Task finished name='Task-2' coro=<BaseApiClient.aclose() done, defined at .../google/genai/_api_client.py:1900> exception=AttributeError("'BaseApiClient' object has no attribute '_http_options'")>
Traceback (most recent call last):
  File ".../google/genai/_api_client.py", line 1904, in aclose
    if not self._http_options.httpx_async_client:
           ^^^^^^^^^^^^^^^^^^
AttributeError: 'BaseApiClient' object has no attribute '_http_options'

Suggested changes

Handle partial initialization gracefully in close() and aclose()
    def close(self) -> None:
    """Closes the API client."""
    # Let users close the custom client explicitly by themselves. Otherwise,
    # close the client when the object is garbage collected.
    # Guard against partial initialization if __init__ failed.

    try:
        options, client = self._http_options, self._httpx_client
    except AttributeError:
        return

    if not options.httpx_client:
        client.close()

  async def aclose(self) -> None:
    """Closes the API async client."""
    # Let users close the custom client explicitly by themselves. Otherwise,
    # close the client when the object is garbage collected.
    # Guard against partial initialization if __init__ failed.

    try:
        options = self._http_options
    except AttributeError:
        return

    try:
        async_http_client = self._async_httpx_client
    except AttributeError:
        async_http_client = None
    if async_http_client and not options.httpx_async_client:
        await async_http_client.aclose()

    try:
        aio_http_session = self._aiohttp_session
    except AttributeError:
        aio_http_session = None
    if aio_http_session and not options.aiohttp_client:
        await aio_http_session.close()

  def __del__(self) -> None:
    """Closes the API client when the object is garbage collected.

    ADK uses this client so cannot rely on the genai.[Async]Client.__del__
    for cleanup.
    """

    try:
      self.close()
    except Exception:  # pylint: disable=broad-except
      pass

    try:
      asyncio.get_running_loop().create_task(self.aclose())
    except Exception:  # pylint: disable=broad-except
      pass

Metadata

Metadata

Assignees

Labels

priority: p2Moderately-important priority. Fix may not be included in next release.type: bugError or flaw in code with unintended results or allowing sub-optimal usage patterns.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions