From 552ba55b9b0e9cb63781dfbedf27edc3b6ee5950 Mon Sep 17 00:00:00 2001 From: 64johnlee <64lamei@gmail.com> Date: Sat, 6 Jun 2026 21:50:37 +0800 Subject: [PATCH 1/2] fix: honour verify=False/ssl=False in HttpOptions client args The three _ensure_*_ssl_ctx helpers guarded the SSL context with `if not ctx`, which evaluated `False` (the falsy boolean the user passes to disable SSL verification) as "no context provided" and silently replaced it with a default strict SSLContext. Changed all three guards to `if ctx is None` so that only a genuinely absent value triggers default context creation. The _maybe_set inner functions received the same treatment: `not args.get(verify)` became `args.get(verify) is None` so an explicit False is never overwritten. Affected methods: - _ensure_httpx_ssl_ctx (verify key, sync + async client args) - _ensure_aiohttp_ssl_ctx (ssl key, async client args) - _ensure_websocket_ssl_ctx (ssl key, async client args) Fixes #2557 Co-Authored-By: Claude Sonnet 4.6 --- google/genai/_api_client.py | 18 +++++----- .../genai/tests/client/test_http_options.py | 36 +++++++++++++++++++ 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/google/genai/_api_client.py b/google/genai/_api_client.py index e6202815e..6bb0b03be 100644 --- a/google/genai/_api_client.py +++ b/google/genai/_api_client.py @@ -988,7 +988,7 @@ def _ensure_httpx_ssl_ctx( else None ) - if not ctx: + if ctx is None: # Initialize the SSL context for the httpx client. # Unlike requests, the httpx package does not automatically pull in the # environment variables SSL_CERT_FILE or SSL_CERT_DIR. They need to be @@ -1000,7 +1000,7 @@ def _ensure_httpx_ssl_ctx( def _maybe_set( args: Optional[_common.StringDict], - ctx: ssl.SSLContext, + ctx, ) -> _common.StringDict: """Sets the SSL context in the client args if not set. @@ -1013,7 +1013,7 @@ def _maybe_set( Returns: The client args with the SSL context included. """ - if not args or not args.get(verify): + if not args or args.get(verify) is None: args = (args or {}).copy() args[verify] = ctx # Drop the args that isn't used by the httpx client. @@ -1044,7 +1044,7 @@ def _ensure_aiohttp_ssl_ctx(options: HttpOptions) -> _common.StringDict: async_args = options.async_client_args ctx = async_args.get(verify) if async_args else None - if not ctx: + if ctx is None: ctx = ssl.create_default_context( cafile=os.environ.get('SSL_CERT_FILE', certifi.where()), capath=os.environ.get('SSL_CERT_DIR'), @@ -1052,7 +1052,7 @@ def _ensure_aiohttp_ssl_ctx(options: HttpOptions) -> _common.StringDict: def _maybe_set( args: Optional[_common.StringDict], - ctx: ssl.SSLContext, + ctx, ) -> _common.StringDict: """Sets the SSL context in the client args if not set. @@ -1065,7 +1065,7 @@ def _maybe_set( Returns: The client args with the SSL context included. """ - if not args or not args.get(verify): + if not args or args.get(verify) is None: args = (args or {}).copy() args[verify] = ctx # Drop the args that isn't in the aiohttp RequestOptions. @@ -1097,7 +1097,7 @@ def _ensure_websocket_ssl_ctx(options: HttpOptions) -> _common.StringDict: async_args = options.async_client_args ctx = async_args.get(verify) if async_args else None - if not ctx: + if ctx is None: # Initialize the SSL context for the httpx client. # Unlike requests, the aiohttp package does not automatically pull in the # environment variables SSL_CERT_FILE or SSL_CERT_DIR. They need to be @@ -1110,7 +1110,7 @@ def _ensure_websocket_ssl_ctx(options: HttpOptions) -> _common.StringDict: def _maybe_set( args: Optional[_common.StringDict], - ctx: ssl.SSLContext, + ctx, ) -> _common.StringDict: """Sets the SSL context in the client args if not set. @@ -1123,7 +1123,7 @@ def _maybe_set( Returns: The client args with the SSL context included. """ - if not args or not args.get(verify): + if not args or args.get(verify) is None: args = (args or {}).copy() args[verify] = ctx # Drop the args that isn't in the aiohttp RequestOptions. diff --git a/google/genai/tests/client/test_http_options.py b/google/genai/tests/client/test_http_options.py index e27808142..f49a996a7 100644 --- a/google/genai/tests/client/test_http_options.py +++ b/google/genai/tests/client/test_http_options.py @@ -180,3 +180,39 @@ def test_base_url_resource_scope_not_set_by_default(): def test_retry_options_not_set_by_default(): options = types.HttpOptions() assert options.retry_options is None + + +def test_ensure_httpx_ssl_ctx_preserves_verify_false(): + """verify=False in client_args must not be overridden by the default SSL context.""" + options = types.HttpOptions(client_args={'verify': False}) + client_args, _ = _api_client.BaseApiClient._ensure_httpx_ssl_ctx(options) + assert client_args.get('verify') is False + + +def test_ensure_httpx_ssl_ctx_preserves_verify_false_async(): + """verify=False in async_client_args must not be overridden.""" + options = types.HttpOptions(async_client_args={'verify': False}) + _, async_client_args = _api_client.BaseApiClient._ensure_httpx_ssl_ctx(options) + assert async_client_args.get('verify') is False + + +def test_ensure_httpx_ssl_ctx_sets_default_ctx_when_absent(): + """When verify is absent, a default SSLContext is injected.""" + import ssl + options = types.HttpOptions() + client_args, _ = _api_client.BaseApiClient._ensure_httpx_ssl_ctx(options) + assert isinstance(client_args.get('verify'), ssl.SSLContext) + + +def test_ensure_aiohttp_ssl_ctx_preserves_ssl_false(): + """ssl=False in async_client_args must not be overridden.""" + options = types.HttpOptions(async_client_args={'ssl': False}) + result = _api_client.BaseApiClient._ensure_aiohttp_ssl_ctx(options) + assert result.get('ssl') is False + + +def test_ensure_websocket_ssl_ctx_preserves_ssl_false(): + """ssl=False in async_client_args must not be overridden for WebSocket.""" + options = types.HttpOptions(async_client_args={'ssl': False}) + result = _api_client.BaseApiClient._ensure_websocket_ssl_ctx(options) + assert result.get('ssl') is False From a102a3a525c8e12d1b76dee9b9c693a0268ca2d4 Mon Sep 17 00:00:00 2001 From: 64JohnLee <64lamei@gmail.com> Date: Tue, 9 Jun 2026 10:57:49 +0800 Subject: [PATCH 2/2] fix(types): resolve mypy strict errors in _api_client.py - Restore type annotation on _maybe_set ctx parameter (widened from ssl.SSLContext to Union[ssl.SSLContext, bool] to allow verify=False) - Add CaseInsensitiveDict[Any] type argument (pre-existing type-arg error) - Add type: ignore[arg-type] on aiohttp.ClientTimeout calls (google-auth stubs incorrectly declare timeout as float) - Add type: ignore[arg-type] on response.headers passed to HttpResponse (multidict stub resolves as Mapping[str, str] rather than CIMultiDictProxy) Co-Authored-By: Claude Sonnet 4.6 --- google/genai/_api_client.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/google/genai/_api_client.py b/google/genai/_api_client.py index 6bb0b03be..d871cf49d 100644 --- a/google/genai/_api_client.py +++ b/google/genai/_api_client.py @@ -250,7 +250,7 @@ def __init__( dict[str, str], httpx.Headers, 'CIMultiDictProxy[str]', - CaseInsensitiveDict, + CaseInsensitiveDict[Any], ], response_stream: Union[Any, str] = None, byte_stream: Union[Any, bytes] = None, @@ -1000,7 +1000,7 @@ def _ensure_httpx_ssl_ctx( def _maybe_set( args: Optional[_common.StringDict], - ctx, + ctx: Union[ssl.SSLContext, bool], ) -> _common.StringDict: """Sets the SSL context in the client args if not set. @@ -1052,7 +1052,7 @@ def _ensure_aiohttp_ssl_ctx(options: HttpOptions) -> _common.StringDict: def _maybe_set( args: Optional[_common.StringDict], - ctx, + ctx: Union[ssl.SSLContext, bool], ) -> _common.StringDict: """Sets the SSL context in the client args if not set. @@ -1110,7 +1110,7 @@ def _ensure_websocket_ssl_ctx(options: HttpOptions) -> _common.StringDict: def _maybe_set( args: Optional[_common.StringDict], - ctx, + ctx: Union[ssl.SSLContext, bool], ) -> _common.StringDict: """Sets the SSL context in the client args if not set. @@ -1445,7 +1445,7 @@ async def _async_request_once( url=url, headers=http_request.headers, data=data, - timeout=aiohttp.ClientTimeout(total=http_request.timeout), + timeout=aiohttp.ClientTimeout(total=http_request.timeout), # type: ignore[arg-type] **self._async_client_session_request_args, ) except ( @@ -1468,7 +1468,7 @@ async def _async_request_once( url=url, headers=http_request.headers, data=data, - timeout=aiohttp.ClientTimeout(total=http_request.timeout), + timeout=aiohttp.ClientTimeout(total=http_request.timeout), # type: ignore[arg-type] **self._async_client_session_request_args, ) @@ -1477,7 +1477,7 @@ async def _async_request_once( # Extract the underlying aiohttp.ClientResponse from the # AsyncAuthorizedSession Response. response = response._response - return HttpResponse(response.headers, response) + return HttpResponse(response.headers, response) # type: ignore[arg-type] else: # aiohttp is not available. Fall back to httpx. httpx_request = self._async_httpx_client.build_request( # type: ignore[union-attr] @@ -1515,7 +1515,7 @@ async def _async_request_once( url=url, headers=http_request.headers, data=data, - timeout=aiohttp.ClientTimeout(total=http_request.timeout), + timeout=aiohttp.ClientTimeout(total=http_request.timeout), # type: ignore[arg-type] **self._async_client_session_request_args, ) await errors.APIError.raise_for_async_response(response) @@ -1546,7 +1546,7 @@ async def _async_request_once( url=url, headers=http_request.headers, data=data, - timeout=aiohttp.ClientTimeout(total=http_request.timeout), + timeout=aiohttp.ClientTimeout(total=http_request.timeout), # type: ignore[arg-type] **self._async_client_session_request_args, ) await errors.APIError.raise_for_async_response(response) @@ -1983,7 +1983,7 @@ async def _async_upload_fd( url=upload_url, data=file_chunk, headers=upload_headers, - timeout=aiohttp.ClientTimeout(total=timeout_in_seconds), + timeout=aiohttp.ClientTimeout(total=timeout_in_seconds), # type: ignore[arg-type] ) if response.headers.get('X-Goog-Upload-Status'): @@ -2014,7 +2014,7 @@ async def _async_upload_fd( 'Failed to upload file: Upload status is not finalized.' ) return HttpResponse( - response.headers, response_stream=[await response.text()] # type: ignore[union-attr] + response.headers, response_stream=[await response.text()] # type: ignore[union-attr, arg-type] ) else: # aiohttp is not available. Fall back to httpx. @@ -2134,12 +2134,12 @@ async def async_download_file( url=http_request.url, headers=http_request.headers, data=data, - timeout=aiohttp.ClientTimeout(total=http_request.timeout), + timeout=aiohttp.ClientTimeout(total=http_request.timeout), # type: ignore[arg-type] ) await errors.APIError.raise_for_async_response(response) return HttpResponse( - response.headers, byte_stream=[await response.read()] + response.headers, byte_stream=[await response.read()] # type: ignore[arg-type] ).byte_stream[0] else: # aiohttp is not available. Fall back to httpx.