diff --git a/dapr/aio/clients/grpc/client.py b/dapr/aio/clients/grpc/client.py index 61924a60a..11aefadf6 100644 --- a/dapr/aio/clients/grpc/client.py +++ b/dapr/aio/clients/grpc/client.py @@ -57,6 +57,7 @@ MetadataTuple, convert_dict_to_grpc_dict_of_any, convert_value_to_struct, + set_default_grpc_dns_resolver, to_bytes, validateNotBlankString, validateNotNone, @@ -189,6 +190,8 @@ def __init__( ) interceptors.append(api_token_interceptor) + set_default_grpc_dns_resolver() + # Create gRPC channel if self._uri.tls: self._channel = grpc.aio.secure_channel( diff --git a/dapr/clients/grpc/_helpers.py b/dapr/clients/grpc/_helpers.py index 1ab1f9e68..fdfb6dbdf 100644 --- a/dapr/clients/grpc/_helpers.py +++ b/dapr/clients/grpc/_helpers.py @@ -13,6 +13,9 @@ limitations under the License. """ +import logging +import os +import sys from enum import Enum from typing import Any, Dict, List, Optional, Tuple, Union, cast @@ -33,6 +36,27 @@ MetadataDict = Dict[str, List[Union[bytes, str]]] MetadataTuple = Tuple[Tuple[str, Union[bytes, str]], ...] +_logger = logging.getLogger(__name__) + +_GRPC_DNS_RESOLVER_ENV = 'GRPC_DNS_RESOLVER' +_GRPC_DNS_RESOLVER_NATIVE = 'native' + + +def set_default_grpc_dns_resolver() -> None: + """Default gRPC to the OS-native DNS resolver on macOS. + + grpcio's bundled c-ares resolver often ignores macOS DNS config and fails + with "DNS query cancelled" for endpoints that resolve fine system-wide. + Resolver choice is process-wide with no per-channel override, so it is set + only on Darwin and only when unset, leaving an explicit value untouched. + """ + if sys.platform != 'darwin': + return + if _GRPC_DNS_RESOLVER_ENV in os.environ: + return + os.environ[_GRPC_DNS_RESOLVER_ENV] = _GRPC_DNS_RESOLVER_NATIVE + _logger.debug('Defaulted %s=%s on macOS', _GRPC_DNS_RESOLVER_ENV, _GRPC_DNS_RESOLVER_NATIVE) + def tuple_to_dict(tupledata: MetadataTuple) -> MetadataDict: """Converts tuple to dict. diff --git a/dapr/clients/grpc/client.py b/dapr/clients/grpc/client.py index c4130329a..16379ff69 100644 --- a/dapr/clients/grpc/client.py +++ b/dapr/clients/grpc/client.py @@ -44,6 +44,7 @@ MetadataTuple, convert_dict_to_grpc_dict_of_any, convert_value_to_struct, + set_default_grpc_dns_resolver, to_bytes, validateNotBlankString, validateNotNone, @@ -169,6 +170,8 @@ def __init__( except ValueError as error: raise DaprInternalError(f'{error}') from error + set_default_grpc_dns_resolver() + if self._uri.tls: self._channel = grpc.secure_channel( # type: ignore self._uri.endpoint, diff --git a/tests/test_grpc_dns_resolver_default.py b/tests/test_grpc_dns_resolver_default.py new file mode 100644 index 000000000..47c3df404 --- /dev/null +++ b/tests/test_grpc_dns_resolver_default.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import unittest +from unittest import mock + +from dapr.clients.grpc import _helpers + +_ENV = _helpers._GRPC_DNS_RESOLVER_ENV +_NATIVE = _helpers._GRPC_DNS_RESOLVER_NATIVE + + +class TestSetDefaultGrpcDnsResolver(unittest.TestCase): + def test_sets_native_on_darwin_when_unset(self): + with ( + mock.patch.object(_helpers.sys, 'platform', 'darwin'), + mock.patch.dict(_helpers.os.environ, {}, clear=True), + ): + _helpers.set_default_grpc_dns_resolver() + self.assertEqual(_helpers.os.environ[_ENV], _NATIVE) + + def test_preserves_explicit_value_on_darwin(self): + with ( + mock.patch.object(_helpers.sys, 'platform', 'darwin'), + mock.patch.dict(_helpers.os.environ, {_ENV: 'ares'}, clear=True), + ): + _helpers.set_default_grpc_dns_resolver() + self.assertEqual(_helpers.os.environ[_ENV], 'ares') + + def test_noop_on_non_darwin(self): + with ( + mock.patch.object(_helpers.sys, 'platform', 'linux'), + mock.patch.dict(_helpers.os.environ, {}, clear=True), + ): + _helpers.set_default_grpc_dns_resolver() + self.assertNotIn(_ENV, _helpers.os.environ) + + +if __name__ == '__main__': + unittest.main()