From 346f7b1e80802f6ed9cb38b08d086067b6a135ed Mon Sep 17 00:00:00 2001 From: x-tahosin Date: Sun, 23 Aug 2026 22:01:47 +0600 Subject: [PATCH] fix: eagerly release http response memory buffers to prevent unbounded growth Fixes #2369. When using generate_content(stream=False), the SDK extracts the text from the response and wraps it in a list inside HttpResponse. However, the underlying httpx.Response or aiohttp.ClientResponse can be kept alive by exceptions or internal async event loop state. For responses containing large strings (e.g. base64 images), this causes a significant memory leak. This change ensures that once the text is read, we explicitly close the connection and clear large string buffers from the response objects. --- google/genai/_api_client.py | 42 +++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/google/genai/_api_client.py b/google/genai/_api_client.py index 3c4cbc247..e882c10d1 100644 --- a/google/genai/_api_client.py +++ b/google/genai/_api_client.py @@ -1509,8 +1509,20 @@ def _request_once( ) response = self._httpx_client.send(httpx_request, stream=stream) # type: ignore[union-attr, arg-type] errors.APIError.raise_for_response(response) + if stream: + return HttpResponse(response.headers, response) + + text = response.text + # Eagerly release memory to prevent unbounded growth (Issue #2369) + if hasattr(response, "close"): + response.close() + if hasattr(response, "_content"): + response._content = b"" + if hasattr(response, "_text"): + response._text = "" + return HttpResponse( - response.headers, response if stream else [response.text] + response.headers, [text] ) def _request( @@ -1656,8 +1668,14 @@ async def _async_request_once( if hasattr(unwrapped_response, '_response'): unwrapped_response = unwrapped_response._response + text = await unwrapped_response.text() + if hasattr(unwrapped_response, 'release'): + unwrapped_response.release() + if hasattr(unwrapped_response, '_body'): + unwrapped_response._body = None + return HttpResponse( - unwrapped_response.headers, [await unwrapped_response.text()] + unwrapped_response.headers, [text] ) except ( aiohttp.ClientConnectorError, @@ -1690,9 +1708,15 @@ async def _async_request_once( if hasattr(unwrapped_retry_response, '_response'): unwrapped_retry_response = unwrapped_retry_response._response + text = await unwrapped_retry_response.text() + if hasattr(unwrapped_retry_response, 'release'): + unwrapped_retry_response.release() + if hasattr(unwrapped_retry_response, '_body'): + unwrapped_retry_response._body = None + return HttpResponse( unwrapped_retry_response.headers, - [await unwrapped_retry_response.text()], + [text], ) else: # aiohttp is not available. Fall back to httpx. @@ -1704,7 +1728,17 @@ async def _async_request_once( timeout=http_request.timeout, ) await errors.APIError.raise_for_async_response(client_response) - return HttpResponse(client_response.headers, [client_response.text]) + + text = client_response.text + # Eagerly release memory to prevent unbounded growth (Issue #2369) + if hasattr(client_response, "aclose"): + await client_response.aclose() + if hasattr(client_response, "_content"): + client_response._content = b"" + if hasattr(client_response, "_text"): + client_response._text = "" + + return HttpResponse(client_response.headers, [text]) async def _async_request( self,