diff --git a/google/genai/_gaos/google_genai.py b/google/genai/_gaos/google_genai.py index 74efdce224..3490a227eb 100644 --- a/google/genai/_gaos/google_genai.py +++ b/google/genai/_gaos/google_genai.py @@ -1279,7 +1279,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, @@ -1437,7 +1438,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..a9ef887336 100644 --- a/google/genai/_gaos/lib/compat_errors.py +++ b/google/genai/_gaos/lib/compat_errors.py @@ -350,9 +350,8 @@ 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] + # Keep httpx errors for retries; wrap_sdk_call translates the final error. + 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..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,6 +115,37 @@ 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( + "operation,method", [("sync", "POST"), ("async", "POST"), ("upload", "PUT")] +) +@pytest.mark.asyncio +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)}, + "async_client_args": {"transport": httpx.MockTransport(handler)}, + "retry_options": {"initial_delay": 0, "max_delay": 0}, + }, + ) + 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) ---