Skip to content

Commit ead356e

Browse files
MarkDaoustcopybara-github
authored andcommitted
feat!: Switch default HTTP client to httpx2 and use OS trust store by default
BREAKING CHANGE: `httpx2` is now the required dependency and default HTTP client instead of `httpx`, and default SSL verification with `httpx2` uses the OS certificate trust store (via `truststore` / `httpx2.create_ssl_context()`) when `SSL_CERT_FILE` and `SSL_CERT_DIR` are not set. To maintain baseline behavior, install `google-genai[httpx]` and set `GOOGLE_GENAI_HTTP_CLIENT=httpx`. Summary of impact: - If your code uses `httpx` or `httpx2` to interact with the SDK, make sure it uses the same package as the active SDK client. - In most cases, updating your code with `import httpx2 as httpx` is sufficient. - The SDK still fully supports both `httpx` and `httpx2`; only the default has changed. - `httpx` is no longer installed by default (use `pip install google-genai[httpx]` to install a compatible version; `google-genai[httpx2]` remains as a no-op extra). - You can set the SDK's default backend with `GOOGLE_GENAI_HTTP_CLIENT` (`httpx` or `httpx2` strictly requires that package; unset or `auto` prefers `httpx2` and falls back to `httpx` if only `httpx` is installed). - Regardless of the default, passing `HttpOptions(httpx_client=..., httpx_async_client=...)` uses those objects directly (sync and async are configured independently). Where `httpx`/`httpx2` types must match the active client: - `HttpOptions(client_args=..., async_client_args=...)`: package-specific objects such as `Timeout`, `MockTransport` / `BaseTransport` (and `Response` objects from mock handlers), and custom `Auth` subclasses must match the active client. Primitives (numeric timeouts, auth tuples, proxy URL strings) work with either. - Transport exceptions on `client.models`, `files`, etc.: network and timeout errors (`TimeoutException`, `ConnectError`, `HTTPError`) are reraised unwrapped after retries, so `except` clauses catching them directly must match the active client. (4xx/5xx `APIError` exceptions and `client.interactions` `APIConnectionError` / `APITimeoutError` are unchanged, though `.response` / `.request` attributes come from the active client.) Fixes: #2680 PiperOrigin-RevId: 1006549935
1 parent 1ac3c91 commit ead356e

51 files changed

Lines changed: 1097 additions & 176 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎README.md‎

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -250,37 +250,37 @@ client = genai.Client(
250250
)
251251
```
252252

253-
### HTTP client option: httpx2
253+
### HTTP client option: httpx
254254

255-
By default, the SDK uses `httpx` for its HTTP transport. You can opt in to
256-
[`httpx2`](https://github.com/pydantic/httpx2) by installing `google-genai[httpx2]`
255+
By default, the SDK uses [`httpx2`](https://github.com/pydantic/httpx2) for its HTTP transport. You can opt in to
256+
legacy `httpx` by installing `google-genai[httpx]`
257257
and setting the `GOOGLE_GENAI_HTTP_CLIENT` environment variable:
258258

259259
```bash
260-
export GOOGLE_GENAI_HTTP_CLIENT=httpx2
260+
export GOOGLE_GENAI_HTTP_CLIENT=httpx
261261
```
262262

263-
Alternatively, you can directly inject an `httpx2.Client` or `httpx2.AsyncClient`
263+
Alternatively, you can directly inject an `httpx.Client` or `httpx.AsyncClient`
264264
via `HttpOptions`:
265265

266266
```python
267-
import httpx2
267+
import httpx
268268
from google import genai
269269
from google.genai import types
270270

271271
client = genai.Client(
272272
http_options=types.HttpOptions(
273-
httpx_client=httpx2.Client(),
274-
httpx_async_client=httpx2.AsyncClient(),
273+
httpx_client=httpx.Client(),
274+
httpx_async_client=httpx.AsyncClient(),
275275
)
276276
)
277277
```
278278

279279
### Faster async client option: Aiohttp
280280

281-
By default we use httpx for both sync and async client implementations. In order
281+
By default we use httpx2 for both sync and async client implementations. In order
282282
to have faster performance, you may install `google-genai[aiohttp]`. In Gen AI
283-
SDK we configure `trust_env=True` to match with the default behavior of httpx.
283+
SDK we configure `trust_env=True` to match with the default behavior of httpx2.
284284
Additional args of `aiohttp.ClientSession.request()` ([see `_RequestOptions` args](https://github.com/aio-libs/aiohttp/blob/v3.12.13/aiohttp/client.py#L170)) can be passed
285285
through the following way:
286286

@@ -295,7 +295,7 @@ client=Client(..., http_options=http_options)
295295

296296
### Proxy
297297

298-
Both httpx and aiohttp libraries use `urllib.request.getproxies` from
298+
Both httpx2 and aiohttp libraries use `urllib.request.getproxies` from
299299
environment variables. Before client initialization, you may set proxy (and
300300
optional `SSL_CERT_FILE`) by setting the environment variables:
301301

@@ -304,8 +304,8 @@ export HTTPS_PROXY='http://username:password@proxy_uri:port'
304304
export SSL_CERT_FILE='client.pem'
305305
```
306306

307-
If you need `socks5` proxy, httpx [supports](https://www.python-httpx.org/advanced/proxies/#socks) `socks5` proxy if you pass it via
308-
args to `httpx.Client()`. You may install `httpx[socks]` to use it.
307+
If you need `socks5` proxy, httpx2 [supports](https://www.python-httpx.org/advanced/proxies/#socks) `socks5` proxy if you pass it via
308+
args to `httpx2.Client()`. You may install `httpx2[socks]` to use it.
309309
Then, you can pass it through the following way:
310310

311311
```python

‎docs/source/index.rst‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -218,9 +218,9 @@ To set the API version to `v1alpha` for the Gemini Developer API:
218218
Faster async client option: Aiohttp
219219
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
220220

221-
By default we use httpx for both sync and async client implementations. In order
221+
By default we use httpx2 for both sync and async client implementations. In order
222222
to have faster performance, you may install `google-genai[aiohttp]`. In Gen AI
223-
SDK we configure `trust_env=True` to match with the default behavior of httpx.
223+
SDK we configure `trust_env=True` to match with the default behavior of httpx2.
224224
Additional args of `aiohttp.ClientSession.request()`
225225
(`see _RequestOptions args <https://github.com/aio-libs/aiohttp/blob/v3.12.13/aiohttp/client.py#L170>`_)
226226
can be passed through the following way:
@@ -237,7 +237,7 @@ can be passed through the following way:
237237
Proxy
238238
^^^^^^^
239239

240-
Both httpx and aiohttp libraries use `urllib.request.getproxies` from
240+
Both httpx2 and aiohttp libraries use `urllib.request.getproxies` from
241241
environment variables. Before client initialization, you may set proxy (and
242242
optional SSL_CERT_FILE) by setting the environment variables:
243243

@@ -248,9 +248,9 @@ optional SSL_CERT_FILE) by setting the environment variables:
248248
export SSL_CERT_FILE='client.pem'
249249
250250
251-
If you need `socks5` proxy, httpx `supports <https://www.python-httpx.org/advanced/proxies/#socks>`_
252-
`socks5` proxy if you pass it via args to httpx.Client(). You may install
253-
`httpx[socks]` to use it. Then you can pass it through the following way:
251+
If you need `socks5` proxy, httpx2 `supports <https://www.python-httpx.org/advanced/proxies/#socks>`_
252+
`socks5` proxy if you pass it via args to httpx2.Client(). You may install
253+
`httpx2[socks]` to use it. Then you can pass it through the following way:
254254

255255
.. code:: python
256256

‎google/genai/_api_client.py‎

Lines changed: 101 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -809,13 +809,43 @@ def __del__(self) -> None:
809809
AsyncHttpx2Client = None # type: ignore[assignment,misc]
810810

811811

812-
def _get_http_client_backend() -> str:
812+
# The HTTP client backend used when GOOGLE_GENAI_HTTP_CLIENT is unset or 'auto'.
813+
# If it is not installed, the other backend is used instead.
814+
_DEFAULT_HTTP_CLIENT_BACKEND = 'httpx2'
815+
816+
817+
def _detect_http_backend(http_options: HttpOptions) -> Optional[str]:
818+
"""Returns 'httpx' or 'httpx2' if `http_options` holds objects from either.
819+
820+
Checks the injected clients and the top-level `client_args` /
821+
`async_client_args` values (e.g. `transport`, `timeout`, `auth`). The MRO is
822+
walked so user subclasses (e.g. of `httpx.AsyncBaseTransport`) are detected.
823+
"""
824+
candidates = [http_options.httpx_client, http_options.httpx_async_client]
825+
candidates += (http_options.client_args or {}).values()
826+
candidates += (http_options.async_client_args or {}).values()
827+
for obj in candidates:
828+
for cls in type(obj).__mro__:
829+
package = (cls.__module__ or '').split('.')[0]
830+
if package == 'httpx2' and httpx2 is not None:
831+
return 'httpx2'
832+
if package == 'httpx' and httpx is not None:
833+
return 'httpx'
834+
return None
835+
836+
837+
def _get_http_client_backend(
838+
http_options: Optional[HttpOptions] = None,
839+
) -> str:
813840
"""Determines whether to use 'httpx' or 'httpx2' as the default HTTP client.
814841
815842
Can be controlled via the GOOGLE_GENAI_HTTP_CLIENT environment variable:
816843
- 'httpx2': Use httpx2 (raises ImportError if not installed).
817844
- 'httpx': Use httpx (raises ImportError if not installed).
818-
- 'auto' or unset: Default to httpx if installed, otherwise fall back to httpx2.
845+
- 'auto' or unset: Auto-detect from `http_options` (`httpx_client`,
846+
`httpx_async_client`, `client_args`, `async_client_args`) if provided,
847+
otherwise use `_DEFAULT_HTTP_CLIENT_BACKEND` if installed, or fall back
848+
to the other backend.
819849
"""
820850
env = os.environ.get('GOOGLE_GENAI_HTTP_CLIENT', '').lower().strip()
821851
if env == 'httpx2':
@@ -837,19 +867,33 @@ def _get_http_client_backend() -> str:
837867
'Unrecognized GOOGLE_GENAI_HTTP_CLIENT=%r; falling back to default.', env
838868
)
839869

840-
if httpx is not None:
841-
return 'httpx'
842-
# httpx is a required dependency so it _should_ always be present in a
843-
# standard install, but if a user has uninstalled httpx in an environment
844-
# that only has httpx2 installed, fall back to httpx2 before raising
870+
if http_options is not None:
871+
detected = _detect_http_backend(http_options)
872+
if detected is not None:
873+
return detected
874+
875+
installed = {'httpx': httpx is not None, 'httpx2': httpx2 is not None}
876+
fallback = 'httpx' if _DEFAULT_HTTP_CLIENT_BACKEND == 'httpx2' else 'httpx2'
877+
# Either backend may be missing (httpx2 is an optional extra, and a user may
878+
# have uninstalled httpx), so fall back to the other one before raising
845879
# ImportError.
846-
if httpx2 is not None:
847-
return 'httpx2'
880+
for backend in (_DEFAULT_HTTP_CLIENT_BACKEND, fallback):
881+
if installed[backend]:
882+
return backend
848883
raise ImportError(
849884
'Neither httpx nor httpx2 is installed. Please install httpx2 or httpx.'
850885
)
851886

852887

888+
def _get_http_client_classes(
889+
http_options: Optional[HttpOptions] = None,
890+
) -> Tuple[Any, Any]:
891+
"""Returns (sync_client_cls, async_client_cls) for the resolved backend."""
892+
if _get_http_client_backend(http_options) == 'httpx2':
893+
return SyncHttpx2Client, AsyncHttpx2Client
894+
return SyncHttpxClient, AsyncHttpxClient
895+
896+
853897
class BaseApiClient:
854898
"""Client for calling HTTP APIs sending and receiving JSON."""
855899

@@ -1068,20 +1112,21 @@ def __init__(
10681112
if self._http_options.headers is not None:
10691113
append_library_version_headers(self._http_options.headers)
10701114

1071-
backend = _get_http_client_backend()
1072-
if backend == 'httpx2':
1073-
sync_client_cls: type[Any] = SyncHttpx2Client
1074-
async_client_cls: type[Any] = AsyncHttpx2Client
1075-
else:
1076-
sync_client_cls = SyncHttpxClient
1077-
async_client_cls = AsyncHttpxClient
1078-
1079-
client_args, async_client_args = self._ensure_httpx_ssl_ctx(
1080-
self._http_options,
1081-
vertexai=bool(self.vertexai),
1082-
client_cls=sync_client_cls,
1083-
async_client_cls=async_client_cls,
1115+
sync_client_cls, async_client_cls = _get_http_client_classes(
1116+
self._http_options
10841117
)
1118+
1119+
if (
1120+
self._http_options.httpx_client
1121+
and self._http_options.httpx_async_client
1122+
):
1123+
client_args: _common.StringDict = {}
1124+
async_client_args: _common.StringDict = {}
1125+
else:
1126+
client_args, async_client_args = self._ensure_httpx_ssl_ctx(
1127+
self._http_options,
1128+
vertexai=bool(self.vertexai),
1129+
)
10851130
self._async_httpx_client_args = async_client_args
10861131
self._authorized_session: Optional['AuthorizedSession'] = None
10871132

@@ -1377,19 +1422,47 @@ def _ensure_httpx_ssl_ctx(
13771422
else None
13781423
)
13791424

1425+
default_sync_cls, default_async_cls = _get_http_client_classes(options)
1426+
sync_cls = client_cls or default_sync_cls
1427+
async_cls = async_client_cls or default_async_cls
1428+
1429+
use_httpx2 = httpx2 is not None and (
1430+
(
1431+
sync_cls is not None
1432+
and isinstance(getattr(httpx2, 'Client', None), type)
1433+
and issubclass(sync_cls, httpx2.Client)
1434+
)
1435+
or (
1436+
async_cls is not None
1437+
and isinstance(getattr(httpx2, 'AsyncClient', None), type)
1438+
and issubclass(async_cls, httpx2.AsyncClient)
1439+
)
1440+
)
1441+
13801442
if ctx is None:
1381-
# Initialize the SSL context for the httpx client.
1443+
# Initialize the SSL context for the httpx/httpx2 client.
13821444
# Unlike requests, the httpx package does not automatically pull in the
13831445
# environment variables SSL_CERT_FILE or SSL_CERT_DIR. They need to be
1384-
# enabled explicitly.
1446+
# enabled explicitly. When using httpx2 without SSL_CERT_FILE or
1447+
# SSL_CERT_DIR, delegate to httpx2.create_ssl_context(), which uses
1448+
# truststore (OS native trust store).
13851449
if vertexai:
13861450
ctx = _get_client_cert_ssl_ctx()
13871451

13881452
if ctx is None:
1389-
ctx = ssl.create_default_context(
1390-
cafile=os.environ.get('SSL_CERT_FILE', certifi.where()),
1391-
capath=os.environ.get('SSL_CERT_DIR'),
1392-
)
1453+
if use_httpx2:
1454+
if os.environ.get('SSL_CERT_FILE') or os.environ.get('SSL_CERT_DIR'):
1455+
ctx = ssl.create_default_context(
1456+
cafile=os.environ.get('SSL_CERT_FILE'),
1457+
capath=os.environ.get('SSL_CERT_DIR'),
1458+
)
1459+
else:
1460+
ctx = httpx2.create_ssl_context()
1461+
else:
1462+
ctx = ssl.create_default_context(
1463+
cafile=os.environ.get('SSL_CERT_FILE', certifi.where()),
1464+
capath=os.environ.get('SSL_CERT_DIR'),
1465+
)
13931466

13941467
def _maybe_set(
13951468
raw_args: Optional[_common.StringDict],
@@ -1432,17 +1505,6 @@ def _maybe_set(
14321505
break
14331506
return args
14341507

1435-
sync_cls = client_cls or (
1436-
SyncHttpxClient
1437-
if SyncHttpxClient is not None
1438-
else (httpx.Client if httpx is not None else None)
1439-
)
1440-
async_cls = async_client_cls or (
1441-
AsyncHttpxClient
1442-
if AsyncHttpxClient is not None
1443-
else (httpx.AsyncClient if httpx is not None else None)
1444-
)
1445-
14461508
return (
14471509
_maybe_set(args, ctx, sync_cls),
14481510
_maybe_set(async_args, ctx, async_cls),

‎google/genai/_gaos/_hooks/adapters.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@
3434
BeforeRequestHook,
3535
)
3636
import asyncio
37-
import httpx
37+
try:
38+
import httpx
39+
except ImportError:
40+
import httpx2 as httpx
3841
from typing import Optional, Tuple, Union
3942

4043
# ============================================================================

‎google/genai/_gaos/_hooks/asyncsdkhooks.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717

1818
"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""
1919

20-
import httpx
20+
try:
21+
import httpx
22+
except ImportError:
23+
import httpx2 as httpx
2124
from .asynctypes import (
2225
AsyncBeforeRequestHook,
2326
AsyncAfterSuccessHook,

‎google/genai/_gaos/_hooks/asynctypes.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@
2424
BeforeRequestContext,
2525
)
2626
from abc import ABC, abstractmethod
27-
import httpx
27+
try:
28+
import httpx
29+
except ImportError:
30+
import httpx2 as httpx
2831
from typing import Optional, Tuple, Union
2932

3033

‎google/genai/_gaos/_hooks/google_genai_auth.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@
2121

2222
from typing import Any, Callable, Mapping, Optional, Union, cast
2323

24-
import httpx
24+
try:
25+
import httpx
26+
except ImportError:
27+
import httpx2 as httpx
2528

2629
from .. import types, utils
2730
from .types import BeforeRequestContext, BeforeRequestHook

‎google/genai/_gaos/_hooks/sdkhooks.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717

1818
"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""
1919

20-
import httpx
20+
try:
21+
import httpx
22+
except ImportError:
23+
import httpx2 as httpx
2124
from .types import (
2225
SDKInitHook,
2326
BeforeRequestContext,

‎google/genai/_gaos/_hooks/types.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@
1919

2020
from ..sdkconfiguration import SDKConfiguration
2121
from abc import ABC, abstractmethod
22-
import httpx
22+
try:
23+
import httpx
24+
except ImportError:
25+
import httpx2 as httpx
2326
from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, Union
2427

2528

‎google/genai/_gaos/_internal.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@
2222
from .basesdk import AsyncBaseSDK, BaseSDK
2323
from .types import OptionalNullable, UNSET
2424
from .utils import get_security_from_env, response_helpers
25-
import httpx
25+
try:
26+
import httpx
27+
except ImportError:
28+
import httpx2 as httpx
2629
from typing import Any, Mapping, Optional, Union, cast
2730

2831

0 commit comments

Comments
 (0)