From 8fc7787a06c66a65ed010658e6bb4e3df42b31a4 Mon Sep 17 00:00:00 2001 From: "Fatih C. Akyon" <34196005+fcakyon@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:26:30 +0300 Subject: [PATCH 1/3] fix: retry connection errors in interactions CompatErrorHook wrapped httpx errors before the retry loop checked them. --- google/genai/_gaos/google_genai.py | 6 +- google/genai/_gaos/lib/compat_errors.py | 6 +- .../tests/interactions/test_httpx_compat.py | 61 +++++++++++++++++++ 3 files changed, 68 insertions(+), 5 deletions(-) diff --git a/google/genai/_gaos/google_genai.py b/google/genai/_gaos/google_genai.py index 95c9fdf7c8..7dfa5c6662 100644 --- a/google/genai/_gaos/google_genai.py +++ b/google/genai/_gaos/google_genai.py @@ -1021,7 +1021,8 @@ def upload( or 'v1alpha' ).lstrip('/') - handshake_res = self._internal.start_upload( + handshake_res = wrap_sdk_call( + self._internal.start_upload, environment=clean_env, path=clean_path, x_goog_upload_header_content_length=size_bytes, @@ -1179,7 +1180,8 @@ async def upload( or 'v1alpha' ).lstrip('/') - handshake_res = await self._internal.start_upload( + handshake_res = await async_wrap_sdk_call( + self._internal.start_upload, environment=clean_env, path=clean_path, x_goog_upload_header_content_length=size_bytes, diff --git a/google/genai/_gaos/lib/compat_errors.py b/google/genai/_gaos/lib/compat_errors.py index 7bd524927e..c25a70db20 100644 --- a/google/genai/_gaos/lib/compat_errors.py +++ b/google/genai/_gaos/lib/compat_errors.py @@ -350,9 +350,9 @@ def after_error( response: Optional[httpx.Response], error: Optional[Exception], ) -> tuple[Optional[httpx.Response], Optional[Exception]]: - if error is None: - return response, error - return response, wrap_sdk_error(error) # type: ignore[return-value] + # Runs once per attempt inside the retry loop, which only retries raw + # httpx errors, so `wrap_sdk_call` translates after the last attempt. + return response, error def after_parse_error( self, diff --git a/google/genai/tests/interactions/test_httpx_compat.py b/google/genai/tests/interactions/test_httpx_compat.py index ea867f6a04..f6f6fdfb76 100644 --- a/google/genai/tests/interactions/test_httpx_compat.py +++ b/google/genai/tests/interactions/test_httpx_compat.py @@ -113,6 +113,67 @@ async def failing_agen(): assert "async stream network broken" in str(exc_info.value) +def test_interactions_retries_connection_errors(): + recorded_requests = [] + + def handler(request: httpx.Request) -> httpx.Response: + recorded_requests.append(request) + raise httpx.ConnectError("connection refused", request=request) + + client = client_lib.Client( + api_key="fake-key", + http_options={ + "client_args": {"transport": httpx.MockTransport(handler)}, + "retry_options": {"attempts": 2, "initial_delay": 0, "max_delay": 0}, + }, + ) + with pytest.raises(compat_errors.APIConnectionError) as exc_info: + client.interactions.create(model="gemini-1.5-flash", input="Hello") + assert len(recorded_requests) > 1 + assert isinstance(exc_info.value.__cause__, httpx.ConnectError) + + +@pytest.mark.asyncio +async def test_async_interactions_retries_connection_errors(): + recorded_requests = [] + + def handler(request: httpx.Request) -> httpx.Response: + recorded_requests.append(request) + raise httpx.ConnectError("connection refused", request=request) + + client = client_lib.Client( + api_key="fake-key", + http_options={ + "async_client_args": {"transport": httpx.MockTransport(handler)}, + "retry_options": {"attempts": 2, "initial_delay": 0, "max_delay": 0}, + }, + ) + with pytest.raises(compat_errors.APIConnectionError) as exc_info: + await client.aio.interactions.create( + model="gemini-1.5-flash", input="Hello" + ) + assert len(recorded_requests) > 1 + assert isinstance(exc_info.value.__cause__, httpx.ConnectError) + + +def test_environment_file_upload_wraps_connection_errors(): + def handler(request: httpx.Request) -> httpx.Response: + raise httpx.ConnectError("connection refused", request=request) + + client = client_lib.Client( + api_key="fake-key", + http_options={ + "client_args": {"transport": httpx.MockTransport(handler)}, + "retry_options": {"attempts": 1, "initial_delay": 0, "max_delay": 0}, + }, + ) + with pytest.raises(compat_errors.APIConnectionError) as exc_info: + client.environments.files.upload( + environment="env_123", path="src/main.py", file=b"print('hello')" + ) + assert isinstance(exc_info.value.__cause__, httpx.ConnectError) + + # --- httpx2 tests (run in CI when httpx2 is installed) --- From 654eff7719fe10cd8995643fe2dcac30d299ae2c Mon Sep 17 00:00:00 2001 From: "Fatih C. Akyon" <34196005+fcakyon@users.noreply.github.com> Date: Wed, 28 Oct 2026 22:10:05 +0300 Subject: [PATCH 2/3] test: cover default interaction timeouts --- .../tests/interactions/test_httpx_compat.py | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/google/genai/tests/interactions/test_httpx_compat.py b/google/genai/tests/interactions/test_httpx_compat.py index f6f6fdfb76..9b6d55cc3e 100644 --- a/google/genai/tests/interactions/test_httpx_compat.py +++ b/google/genai/tests/interactions/test_httpx_compat.py @@ -113,24 +113,28 @@ async def failing_agen(): assert "async stream network broken" in str(exc_info.value) -def test_interactions_retries_connection_errors(): +@pytest.mark.parametrize( + "error_type,api_error_type", + [ + (httpx.ConnectError, compat_errors.APIConnectionError), + (httpx.ReadTimeout, compat_errors.APITimeoutError), + ], +) +def test_interactions_retries_transport_errors(error_type, api_error_type): recorded_requests = [] def handler(request: httpx.Request) -> httpx.Response: recorded_requests.append(request) - raise httpx.ConnectError("connection refused", request=request) + raise error_type("request failed", request=request) client = client_lib.Client( api_key="fake-key", - http_options={ - "client_args": {"transport": httpx.MockTransport(handler)}, - "retry_options": {"attempts": 2, "initial_delay": 0, "max_delay": 0}, - }, + http_options={"client_args": {"transport": httpx.MockTransport(handler)}}, ) - with pytest.raises(compat_errors.APIConnectionError) as exc_info: + with pytest.raises(api_error_type) as exc_info: client.interactions.create(model="gemini-1.5-flash", input="Hello") - assert len(recorded_requests) > 1 - assert isinstance(exc_info.value.__cause__, httpx.ConnectError) + assert [request.method for request in recorded_requests] == ["POST"] * 4 + assert isinstance(exc_info.value.__cause__, error_type) @pytest.mark.asyncio From 0f82dd892a1540798acb0df49b5217a1efe2287d Mon Sep 17 00:00:00 2001 From: "Fatih C. Akyon" <34196005+fcakyon@users.noreply.github.com> Date: Tue, 3 Nov 2026 00:47:48 +0300 Subject: [PATCH 3/3] test: share transport error coverage --- google/genai/_gaos/lib/compat_errors.py | 3 +- .../tests/interactions/test_httpx_compat.py | 76 ++++++------------- 2 files changed, 23 insertions(+), 56 deletions(-) diff --git a/google/genai/_gaos/lib/compat_errors.py b/google/genai/_gaos/lib/compat_errors.py index c25a70db20..a9ef887336 100644 --- a/google/genai/_gaos/lib/compat_errors.py +++ b/google/genai/_gaos/lib/compat_errors.py @@ -350,8 +350,7 @@ def after_error( response: Optional[httpx.Response], error: Optional[Exception], ) -> tuple[Optional[httpx.Response], Optional[Exception]]: - # Runs once per attempt inside the retry loop, which only retries raw - # httpx errors, so `wrap_sdk_call` translates after the last attempt. + # Keep httpx errors for retries; wrap_sdk_call translates the final error. return response, error def after_parse_error( diff --git a/google/genai/tests/interactions/test_httpx_compat.py b/google/genai/tests/interactions/test_httpx_compat.py index 9b6d55cc3e..e42db959f5 100644 --- a/google/genai/tests/interactions/test_httpx_compat.py +++ b/google/genai/tests/interactions/test_httpx_compat.py @@ -14,6 +14,8 @@ """Tests for httpx and httpx2 compatibility across the GAOS client.""" +from unittest import mock + import httpx import pytest @@ -113,69 +115,35 @@ async def failing_agen(): assert "async stream network broken" in str(exc_info.value) +@pytest.mark.parametrize("error_type,api_error_type", [ + (httpx.ConnectError, compat_errors.APIConnectionError), + (httpx.ReadTimeout, compat_errors.APITimeoutError), +]) @pytest.mark.parametrize( - "error_type,api_error_type", - [ - (httpx.ConnectError, compat_errors.APIConnectionError), - (httpx.ReadTimeout, compat_errors.APITimeoutError), - ], + "operation,method", [("sync", "POST"), ("async", "POST"), ("upload", "PUT")] ) -def test_interactions_retries_transport_errors(error_type, api_error_type): - recorded_requests = [] - - def handler(request: httpx.Request) -> httpx.Response: - recorded_requests.append(request) - raise error_type("request failed", request=request) - - client = client_lib.Client( - api_key="fake-key", - http_options={"client_args": {"transport": httpx.MockTransport(handler)}}, - ) - with pytest.raises(api_error_type) as exc_info: - client.interactions.create(model="gemini-1.5-flash", input="Hello") - assert [request.method for request in recorded_requests] == ["POST"] * 4 - assert isinstance(exc_info.value.__cause__, error_type) - - @pytest.mark.asyncio -async def test_async_interactions_retries_connection_errors(): - recorded_requests = [] - - def handler(request: httpx.Request) -> httpx.Response: - recorded_requests.append(request) - raise httpx.ConnectError("connection refused", request=request) - - client = client_lib.Client( - api_key="fake-key", - http_options={ - "async_client_args": {"transport": httpx.MockTransport(handler)}, - "retry_options": {"attempts": 2, "initial_delay": 0, "max_delay": 0}, - }, - ) - with pytest.raises(compat_errors.APIConnectionError) as exc_info: - await client.aio.interactions.create( - model="gemini-1.5-flash", input="Hello" - ) - assert len(recorded_requests) > 1 - assert isinstance(exc_info.value.__cause__, httpx.ConnectError) - - -def test_environment_file_upload_wraps_connection_errors(): - def handler(request: httpx.Request) -> httpx.Response: - raise httpx.ConnectError("connection refused", request=request) - +async def test_transport_errors(operation, method, error_type, api_error_type): + handler = mock.Mock(side_effect=error_type("request failed")) client = client_lib.Client( api_key="fake-key", http_options={ "client_args": {"transport": httpx.MockTransport(handler)}, - "retry_options": {"attempts": 1, "initial_delay": 0, "max_delay": 0}, + "async_client_args": {"transport": httpx.MockTransport(handler)}, + "retry_options": {"initial_delay": 0, "max_delay": 0}, }, ) - with pytest.raises(compat_errors.APIConnectionError) as exc_info: - client.environments.files.upload( - environment="env_123", path="src/main.py", file=b"print('hello')" - ) - assert isinstance(exc_info.value.__cause__, httpx.ConnectError) + with pytest.raises(api_error_type) as exc_info: + if operation == "upload": + client.environments.files.upload( + environment="env_123", path="src/main.py", file=b"print(1)" + ) + elif operation == "async": + await client.aio.interactions.create(model="gemini-1.5-flash", input="Hello") + else: + client.interactions.create(model="gemini-1.5-flash", input="Hello") + assert [call.args[0].method for call in handler.call_args_list] == [method] * 4 + assert isinstance(exc_info.value.__cause__, error_type) # --- httpx2 tests (run in CI when httpx2 is installed) ---