Skip to content

Commit 425681d

Browse files
authored
fix(grpc): default to native dns resolver on macos (#1125)
* fix: address mac dns resolver issue Signed-off-by: Samantha Coyle <sam@diagrid.io> * fix: scope down to grpc client helpers Signed-off-by: Samantha Coyle <sam@diagrid.io> --------- Signed-off-by: Samantha Coyle <sam@diagrid.io>
1 parent 126c2fa commit 425681d

4 files changed

Lines changed: 82 additions & 0 deletions

File tree

dapr/aio/clients/grpc/client.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
MetadataTuple,
5858
convert_dict_to_grpc_dict_of_any,
5959
convert_value_to_struct,
60+
set_default_grpc_dns_resolver,
6061
to_bytes,
6162
validateNotBlankString,
6263
validateNotNone,
@@ -189,6 +190,8 @@ def __init__(
189190
)
190191
interceptors.append(api_token_interceptor)
191192

193+
set_default_grpc_dns_resolver()
194+
192195
# Create gRPC channel
193196
if self._uri.tls:
194197
self._channel = grpc.aio.secure_channel(

dapr/clients/grpc/_helpers.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
limitations under the License.
1414
"""
1515

16+
import logging
17+
import os
18+
import sys
1619
from enum import Enum
1720
from typing import Any, Dict, List, Optional, Tuple, Union, cast
1821

@@ -33,6 +36,27 @@
3336
MetadataDict = Dict[str, List[Union[bytes, str]]]
3437
MetadataTuple = Tuple[Tuple[str, Union[bytes, str]], ...]
3538

39+
_logger = logging.getLogger(__name__)
40+
41+
_GRPC_DNS_RESOLVER_ENV = 'GRPC_DNS_RESOLVER'
42+
_GRPC_DNS_RESOLVER_NATIVE = 'native'
43+
44+
45+
def set_default_grpc_dns_resolver() -> None:
46+
"""Default gRPC to the OS-native DNS resolver on macOS.
47+
48+
grpcio's bundled c-ares resolver often ignores macOS DNS config and fails
49+
with "DNS query cancelled" for endpoints that resolve fine system-wide.
50+
Resolver choice is process-wide with no per-channel override, so it is set
51+
only on Darwin and only when unset, leaving an explicit value untouched.
52+
"""
53+
if sys.platform != 'darwin':
54+
return
55+
if _GRPC_DNS_RESOLVER_ENV in os.environ:
56+
return
57+
os.environ[_GRPC_DNS_RESOLVER_ENV] = _GRPC_DNS_RESOLVER_NATIVE
58+
_logger.debug('Defaulted %s=%s on macOS', _GRPC_DNS_RESOLVER_ENV, _GRPC_DNS_RESOLVER_NATIVE)
59+
3660

3761
def tuple_to_dict(tupledata: MetadataTuple) -> MetadataDict:
3862
"""Converts tuple to dict.

dapr/clients/grpc/client.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
MetadataTuple,
4545
convert_dict_to_grpc_dict_of_any,
4646
convert_value_to_struct,
47+
set_default_grpc_dns_resolver,
4748
to_bytes,
4849
validateNotBlankString,
4950
validateNotNone,
@@ -169,6 +170,8 @@ def __init__(
169170
except ValueError as error:
170171
raise DaprInternalError(f'{error}') from error
171172

173+
set_default_grpc_dns_resolver()
174+
172175
if self._uri.tls:
173176
self._channel = grpc.secure_channel( # type: ignore
174177
self._uri.endpoint,
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# -*- coding: utf-8 -*-
2+
3+
"""
4+
Copyright 2026 The Dapr Authors
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
"""
15+
16+
import unittest
17+
from unittest import mock
18+
19+
from dapr.clients.grpc import _helpers
20+
21+
_ENV = _helpers._GRPC_DNS_RESOLVER_ENV
22+
_NATIVE = _helpers._GRPC_DNS_RESOLVER_NATIVE
23+
24+
25+
class TestSetDefaultGrpcDnsResolver(unittest.TestCase):
26+
def test_sets_native_on_darwin_when_unset(self):
27+
with (
28+
mock.patch.object(_helpers.sys, 'platform', 'darwin'),
29+
mock.patch.dict(_helpers.os.environ, {}, clear=True),
30+
):
31+
_helpers.set_default_grpc_dns_resolver()
32+
self.assertEqual(_helpers.os.environ[_ENV], _NATIVE)
33+
34+
def test_preserves_explicit_value_on_darwin(self):
35+
with (
36+
mock.patch.object(_helpers.sys, 'platform', 'darwin'),
37+
mock.patch.dict(_helpers.os.environ, {_ENV: 'ares'}, clear=True),
38+
):
39+
_helpers.set_default_grpc_dns_resolver()
40+
self.assertEqual(_helpers.os.environ[_ENV], 'ares')
41+
42+
def test_noop_on_non_darwin(self):
43+
with (
44+
mock.patch.object(_helpers.sys, 'platform', 'linux'),
45+
mock.patch.dict(_helpers.os.environ, {}, clear=True),
46+
):
47+
_helpers.set_default_grpc_dns_resolver()
48+
self.assertNotIn(_ENV, _helpers.os.environ)
49+
50+
51+
if __name__ == '__main__':
52+
unittest.main()

0 commit comments

Comments
 (0)