From 5c7fc54832b87a57ca32912955c520a2c7b8857e Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Thu, 23 Oct 2025 17:01:30 -0500 Subject: [PATCH 1/2] allow to create clients with different dapr-api-token, fallback to original env var DAPR_API_TOKEN if found Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- dapr/aio/clients/__init__.py | 7 +- dapr/aio/clients/grpc/client.py | 14 +- dapr/clients/__init__.py | 7 +- dapr/clients/grpc/client.py | 14 +- dapr/clients/health.py | 13 +- dapr/clients/http/client.py | 14 +- dapr/clients/http/dapr_actor_http_client.py | 5 +- .../http/dapr_invocation_http_client.py | 5 +- tests/clients/test_dapr_grpc_client.py | 34 +++ tests/clients/test_dapr_grpc_client_async.py | 34 +++ tests/clients/test_heatlhcheck.py | 18 ++ tests/clients/test_multi_token_clients.py | 232 ++++++++++++++++++ 12 files changed, 376 insertions(+), 21 deletions(-) create mode 100644 tests/clients/test_multi_token_clients.py diff --git a/dapr/aio/clients/__init__.py b/dapr/aio/clients/__init__.py index e945b130..a07dc471 100644 --- a/dapr/aio/clients/__init__.py +++ b/dapr/aio/clients/__init__.py @@ -66,6 +66,7 @@ def __init__( ] = None, http_timeout_seconds: Optional[int] = None, max_grpc_message_length: Optional[int] = None, + api_token: Optional[str] = None, ): """Connects to Dapr Runtime and via gRPC and HTTP. @@ -79,8 +80,10 @@ def __init__( http_timeout_seconds (int): specify a timeout for http connections max_grpc_messsage_length (int, optional): The maximum grpc send and receive message length in bytes. + api_token (str, optional): Dapr API token for authentication. If not provided, + falls back to DAPR_API_TOKEN environment variable. """ - super().__init__(address, interceptors, max_grpc_message_length) + super().__init__(address, interceptors, max_grpc_message_length, api_token=api_token) self.invocation_client = None invocation_protocol = settings.DAPR_API_METHOD_INVOCATION_PROTOCOL.upper() @@ -89,7 +92,7 @@ def __init__( if http_timeout_seconds is None: http_timeout_seconds = settings.DAPR_HTTP_TIMEOUT_SECONDS self.invocation_client = DaprInvocationHttpClient( - headers_callback=headers_callback, timeout=http_timeout_seconds + headers_callback=headers_callback, timeout=http_timeout_seconds, api_token=api_token ) elif invocation_protocol == 'GRPC': pass diff --git a/dapr/aio/clients/grpc/client.py b/dapr/aio/clients/grpc/client.py index 995b8268..58b64ffb 100644 --- a/dapr/aio/clients/grpc/client.py +++ b/dapr/aio/clients/grpc/client.py @@ -141,6 +141,7 @@ def __init__( ] = None, max_grpc_message_length: Optional[int] = None, retry_policy: Optional[RetryPolicy] = None, + api_token: Optional[str] = None, ): """Connects to Dapr Runtime and initialize gRPC client stub. @@ -152,8 +153,13 @@ def __init__( StreamStreamClientInterceptor, optional): gRPC interceptors. max_grpc_message_length (int, optional): The maximum grpc send and receive message length in bytes. + api_token (str, optional): Dapr API token for authentication. If not provided, + falls back to DAPR_API_TOKEN environment variable. """ - DaprHealth.wait_until_ready() + self._api_token = api_token + # For health check, use explicit token or fall back to global setting + health_token = api_token if api_token is not None else settings.DAPR_API_TOKEN + DaprHealth.wait_until_ready(api_token=health_token) self.retry_policy = retry_policy or RetryPolicy() useragent = f'dapr-sdk-python/{__version__}' @@ -184,10 +190,12 @@ def __init__( else: interceptors.append(DaprClientTimeoutInterceptorAsync()) - if settings.DAPR_API_TOKEN: + # Use explicit token if provided, otherwise fall back to global setting + token = self._api_token if self._api_token is not None else settings.DAPR_API_TOKEN + if token: api_token_interceptor = DaprClientInterceptorAsync( [ - ('dapr-api-token', settings.DAPR_API_TOKEN), + ('dapr-api-token', token), ] ) interceptors.append(api_token_interceptor) diff --git a/dapr/clients/__init__.py b/dapr/clients/__init__.py index 78ad99eb..bcbd8627 100644 --- a/dapr/clients/__init__.py +++ b/dapr/clients/__init__.py @@ -71,6 +71,7 @@ def __init__( http_timeout_seconds: Optional[int] = None, max_grpc_message_length: Optional[int] = None, retry_policy: Optional[RetryPolicy] = None, + api_token: Optional[str] = None, ): """Connects to Dapr Runtime via gRPC and HTTP. @@ -84,8 +85,10 @@ def __init__( http_timeout_seconds (int): specify a timeout for http connections max_grpc_message_length (int, optional): The maximum grpc send and receive message length in bytes. + api_token (str, optional): Dapr API token for authentication. If not provided, + falls back to DAPR_API_TOKEN environment variable. """ - super().__init__(address, interceptors, max_grpc_message_length, retry_policy) + super().__init__(address, interceptors, max_grpc_message_length, retry_policy, api_token) self.invocation_client = None invocation_protocol = settings.DAPR_API_METHOD_INVOCATION_PROTOCOL.upper() @@ -94,7 +97,7 @@ def __init__( if http_timeout_seconds is None: http_timeout_seconds = settings.DAPR_HTTP_TIMEOUT_SECONDS self.invocation_client = DaprInvocationHttpClient( - headers_callback=headers_callback, timeout=http_timeout_seconds + headers_callback=headers_callback, timeout=http_timeout_seconds, api_token=api_token ) elif invocation_protocol == 'GRPC': pass diff --git a/dapr/clients/grpc/client.py b/dapr/clients/grpc/client.py index e4ffb264..0f5b064e 100644 --- a/dapr/clients/grpc/client.py +++ b/dapr/clients/grpc/client.py @@ -132,6 +132,7 @@ def __init__( ] = None, max_grpc_message_length: Optional[int] = None, retry_policy: Optional[RetryPolicy] = None, + api_token: Optional[str] = None, ): """Connects to Dapr Runtime and initializes gRPC client stub. @@ -144,8 +145,13 @@ def __init__( max_grpc_message_length (int, optional): The maximum grpc send and receive message length in bytes. retry_policy (RetryPolicy optional): Specifies retry behaviour + api_token (str, optional): Dapr API token for authentication. If not provided, + falls back to DAPR_API_TOKEN environment variable. """ - DaprHealth.wait_until_ready() + self._api_token = api_token + # For health check, use explicit token or fall back to global setting + health_token = api_token if api_token is not None else settings.DAPR_API_TOKEN + DaprHealth.wait_until_ready(api_token=health_token) self.retry_policy = retry_policy or RetryPolicy() useragent = f'dapr-sdk-python/{__version__}' @@ -184,10 +190,12 @@ def __init__( self._channel = grpc.intercept_channel(self._channel, DaprClientTimeoutInterceptor()) # type: ignore - if settings.DAPR_API_TOKEN: + # Use explicit token if provided, otherwise fall back to global setting + token = self._api_token if self._api_token is not None else settings.DAPR_API_TOKEN + if token: api_token_interceptor = DaprClientInterceptor( [ - ('dapr-api-token', settings.DAPR_API_TOKEN), + ('dapr-api-token', token), ] ) self._channel = grpc.intercept_channel( # type: ignore diff --git a/dapr/clients/health.py b/dapr/clients/health.py index e3daec79..b61e2ab1 100644 --- a/dapr/clients/health.py +++ b/dapr/clients/health.py @@ -12,22 +12,23 @@ See the License for the specific language governing permissions and limitations under the License. """ -import urllib.request -import urllib.error import time +import urllib.error +import urllib.request -from dapr.clients.http.conf import DAPR_API_TOKEN_HEADER, USER_AGENT_HEADER, DAPR_USER_AGENT +from dapr.clients.http.conf import DAPR_API_TOKEN_HEADER, DAPR_USER_AGENT, USER_AGENT_HEADER from dapr.clients.http.helpers import get_api_url from dapr.conf import settings class DaprHealth: @staticmethod - def wait_until_ready(): + def wait_until_ready(api_token: str = None): health_url = f'{get_api_url()}/healthz/outbound' headers = {USER_AGENT_HEADER: DAPR_USER_AGENT} - if settings.DAPR_API_TOKEN is not None: - headers[DAPR_API_TOKEN_HEADER] = settings.DAPR_API_TOKEN + token = api_token if api_token is not None else settings.DAPR_API_TOKEN + if token is not None: + headers[DAPR_API_TOKEN_HEADER] = token timeout = float(settings.DAPR_HEALTH_TIMEOUT) start = time.time() diff --git a/dapr/clients/http/client.py b/dapr/clients/http/client.py index 86e9ab6f..4769b932 100644 --- a/dapr/clients/http/client.py +++ b/dapr/clients/http/client.py @@ -43,6 +43,7 @@ def __init__( timeout: Optional[int] = 60, headers_callback: Optional[Callable[[], Dict[str, str]]] = None, retry_policy: Optional[RetryPolicy] = None, + api_token: Optional[str] = None, ): """Invokes Dapr over HTTP. @@ -50,8 +51,13 @@ def __init__( message_serializer (Serializer): Dapr serializer. timeout (int, optional): Timeout in seconds, defaults to 60. headers_callback (lambda: Dict[str, str]], optional): Generates header for each request. + api_token (str, optional): Dapr API token for authentication. If not provided, + falls back to DAPR_API_TOKEN environment variable. """ - DaprHealth.wait_until_ready() + self._api_token = api_token + # For health check, use explicit token or fall back to global setting + health_token = api_token if api_token is not None else settings.DAPR_API_TOKEN + DaprHealth.wait_until_ready(api_token=health_token) self._timeout = aiohttp.ClientTimeout(total=timeout) self._serializer = message_serializer @@ -71,8 +77,10 @@ async def send_bytes( if not headers_map.get(CONTENT_TYPE_HEADER): headers_map[CONTENT_TYPE_HEADER] = DEFAULT_JSON_CONTENT_TYPE - if settings.DAPR_API_TOKEN is not None: - headers_map[DAPR_API_TOKEN_HEADER] = settings.DAPR_API_TOKEN + # Use explicit token if provided, otherwise fall back to global setting + token = self._api_token if self._api_token is not None else settings.DAPR_API_TOKEN + if token is not None: + headers_map[DAPR_API_TOKEN_HEADER] = token if self._headers_callback is not None: trace_headers = self._headers_callback() diff --git a/dapr/clients/http/dapr_actor_http_client.py b/dapr/clients/http/dapr_actor_http_client.py index 186fdbc1..cfc3ff44 100644 --- a/dapr/clients/http/dapr_actor_http_client.py +++ b/dapr/clients/http/dapr_actor_http_client.py @@ -36,6 +36,7 @@ def __init__( timeout: int = 60, headers_callback: Optional[Callable[[], Dict[str, str]]] = None, retry_policy: Optional[RetryPolicy] = None, + api_token: Optional[str] = None, ): """Invokes Dapr Actors over HTTP. @@ -44,8 +45,10 @@ def __init__( timeout (int, optional): Timeout in seconds, defaults to 60. headers_callback (lambda: Dict[str, str]], optional): Generates header for each request. retry_policy (RetryPolicy optional): Specifies retry behaviour + api_token (str, optional): Dapr API token for authentication. If not provided, + falls back to DAPR_API_TOKEN environment variable. """ - self._client = DaprHttpClient(message_serializer, timeout, headers_callback, retry_policy) + self._client = DaprHttpClient(message_serializer, timeout, headers_callback, retry_policy, api_token=api_token) async def invoke_method( self, actor_type: str, actor_id: str, method: str, data: Optional[bytes] = None diff --git a/dapr/clients/http/dapr_invocation_http_client.py b/dapr/clients/http/dapr_invocation_http_client.py index df4e6d22..b816fabd 100644 --- a/dapr/clients/http/dapr_invocation_http_client.py +++ b/dapr/clients/http/dapr_invocation_http_client.py @@ -39,6 +39,7 @@ def __init__( timeout: int = 60, headers_callback: Optional[Callable[[], Dict[str, str]]] = None, retry_policy: Optional[RetryPolicy] = None, + api_token: Optional[str] = None, ): """Invokes Dapr's API for method invocation over HTTP. @@ -46,9 +47,11 @@ def __init__( timeout (int, optional): Timeout in seconds, defaults to 60. headers_callback (lambda: Dict[str, str]], optional): Generates header for each request. retry_policy (RetryPolicy optional): Specifies retry behaviour + api_token (str, optional): Dapr API token for authentication. If not provided, + falls back to DAPR_API_TOKEN environment variable. """ self._client = DaprHttpClient( - DefaultJSONSerializer(), timeout, headers_callback, retry_policy=retry_policy + DefaultJSONSerializer(), timeout, headers_callback, retry_policy=retry_policy, api_token=api_token ) async def invoke_method_async( diff --git a/tests/clients/test_dapr_grpc_client.py b/tests/clients/test_dapr_grpc_client.py index e0713f70..61abf639 100644 --- a/tests/clients/test_dapr_grpc_client.py +++ b/tests/clients/test_dapr_grpc_client.py @@ -413,6 +413,40 @@ def test_dapr_api_token_insertion(self): self.assertEqual(['value1'], resp.headers['hkey1']) self.assertEqual(['test-token'], resp.headers['hdapr-api-token']) + def test_explicit_api_token(self): + """Test that explicit api_token parameter is used in client""" + dapr = DaprGrpcClient( + f'{self.scheme}localhost:{self.grpc_port}', api_token='explicit-token' + ) + resp = dapr.invoke_method( + app_id='targetId', + method_name='bytes', + data=b'test', + content_type='text/plain', + metadata=(('key1', 'value1'),), + ) + + self.assertEqual(b'test', resp.data) + self.assertEqual('text/plain', resp.content_type) + self.assertEqual(['explicit-token'], resp.headers['hdapr-api-token']) + + @patch.object(settings, 'DAPR_API_TOKEN', 'global-token') + def test_explicit_api_token_overrides_global(self): + """Test that explicit api_token parameter overrides global setting""" + dapr = DaprGrpcClient( + f'{self.scheme}localhost:{self.grpc_port}', api_token='override-token' + ) + resp = dapr.invoke_method( + app_id='targetId', + method_name='bytes', + data=b'test', + content_type='text/plain', + ) + + self.assertEqual(b'test', resp.data) + # Should use explicit token, not global + self.assertEqual(['override-token'], resp.headers['hdapr-api-token']) + def test_get_save_delete_state(self): dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') key = 'key_1' diff --git a/tests/clients/test_dapr_grpc_client_async.py b/tests/clients/test_dapr_grpc_client_async.py index 50043912..6ba30f1b 100644 --- a/tests/clients/test_dapr_grpc_client_async.py +++ b/tests/clients/test_dapr_grpc_client_async.py @@ -406,6 +406,40 @@ async def test_dapr_api_token_insertion(self): self.assertEqual(['value1'], resp.headers['hkey1']) self.assertEqual(['test-token'], resp.headers['hdapr-api-token']) + async def test_explicit_api_token(self): + """Test that explicit api_token parameter is used in client""" + dapr = DaprGrpcClientAsync( + f'{self.scheme}localhost:{self.grpc_port}', api_token='explicit-token' + ) + resp = await dapr.invoke_method( + app_id='targetId', + method_name='bytes', + data=b'test', + content_type='text/plain', + metadata=(('key1', 'value1'),), + ) + + self.assertEqual(b'test', resp.data) + self.assertEqual('text/plain', resp.content_type) + self.assertEqual(['explicit-token'], resp.headers['hdapr-api-token']) + + @patch.object(settings, 'DAPR_API_TOKEN', 'global-token') + async def test_explicit_api_token_overrides_global(self): + """Test that explicit api_token parameter overrides global setting""" + dapr = DaprGrpcClientAsync( + f'{self.scheme}localhost:{self.grpc_port}', api_token='override-token' + ) + resp = await dapr.invoke_method( + app_id='targetId', + method_name='bytes', + data=b'test', + content_type='text/plain', + ) + + self.assertEqual(b'test', resp.data) + # Should use explicit token, not global + self.assertEqual(['override-token'], resp.headers['hdapr-api-token']) + async def test_get_save_delete_state(self): dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') key = 'key_1' diff --git a/tests/clients/test_heatlhcheck.py b/tests/clients/test_heatlhcheck.py index f3be8a47..4799cff9 100644 --- a/tests/clients/test_heatlhcheck.py +++ b/tests/clients/test_heatlhcheck.py @@ -62,6 +62,24 @@ def test_wait_until_ready_success_with_api_token(self, mock_urlopen): self.assertIn('Dapr-api-token', headers) self.assertEqual(headers['Dapr-api-token'], 'mytoken') + @patch.object(settings, 'DAPR_HTTP_ENDPOINT', 'http://domain.com:3500') + @patch('urllib.request.urlopen') + def test_wait_until_ready_with_explicit_token(self, mock_urlopen): + """Test that explicit api_token parameter overrides global setting""" + mock_urlopen.return_value.__enter__.return_value = MagicMock(status=200) + + try: + DaprHealth.wait_until_ready(api_token='explicit-token') + except Exception as e: + self.fail(f'wait_until_ready() raised an exception unexpectedly: {e}') + + mock_urlopen.assert_called_once() + + # Check headers are properly set + headers = mock_urlopen.call_args[0][0].headers + self.assertIn('Dapr-api-token', headers) + self.assertEqual(headers['Dapr-api-token'], 'explicit-token') + @patch.object(settings, 'DAPR_HEALTH_TIMEOUT', '2.5') @patch('urllib.request.urlopen') def test_wait_until_ready_timeout(self, mock_urlopen): diff --git a/tests/clients/test_multi_token_clients.py b/tests/clients/test_multi_token_clients.py new file mode 100644 index 00000000..f86e3862 --- /dev/null +++ b/tests/clients/test_multi_token_clients.py @@ -0,0 +1,232 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2024 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.mock import patch + +from dapr.aio.clients.grpc.client import DaprGrpcClientAsync +from dapr.clients.grpc.client import DaprGrpcClient +from dapr.clients.health import DaprHealth +from dapr.conf import settings + +from .fake_dapr_server import FakeDaprSidecar + + +class MultiTokenClientTests(unittest.TestCase): + """Integration tests for multiple clients with different API tokens""" + + grpc_port_1 = 50011 + grpc_port_2 = 50012 + http_port_1 = 3501 + http_port_2 = 3502 + + @classmethod + def setUpClass(cls): + """Set up two fake Dapr sidecars to simulate different instances""" + cls._fake_dapr_server_1 = FakeDaprSidecar( + grpc_port=cls.grpc_port_1, http_port=cls.http_port_1 + ) + cls._fake_dapr_server_2 = FakeDaprSidecar( + grpc_port=cls.grpc_port_2, http_port=cls.http_port_2 + ) + cls._fake_dapr_server_1.start() + cls._fake_dapr_server_2.start() + + # Set default HTTP endpoint to first server for health checks + settings.DAPR_HTTP_PORT = cls.http_port_1 + settings.DAPR_HTTP_ENDPOINT = f'http://127.0.0.1:{cls.http_port_1}' + + @classmethod + def tearDownClass(cls): + """Clean up fake servers""" + cls._fake_dapr_server_1.stop() + cls._fake_dapr_server_2.stop() + + @patch.object(DaprHealth, 'wait_until_ready') + def test_multiple_sync_clients_different_tokens(self, mock_health): + """Test that multiple synchronous clients can use different tokens""" + # Mock health check to avoid connection issues + mock_health.return_value = None + + # Create two clients with different tokens + client1 = DaprGrpcClient(f'localhost:{self.grpc_port_1}', api_token='token-client-1') + client2 = DaprGrpcClient(f'localhost:{self.grpc_port_2}', api_token='token-client-2') + + try: + # Make requests with both clients + resp1 = client1.invoke_method( + app_id='app1', + method_name='test', + data=b'client1', + content_type='text/plain', + ) + + resp2 = client2.invoke_method( + app_id='app2', + method_name='test', + data=b'client2', + content_type='text/plain', + ) + + # Verify each client used its own token + self.assertEqual(b'client1', resp1.data) + self.assertEqual(['token-client-1'], resp1.headers['hdapr-api-token']) + + self.assertEqual(b'client2', resp2.data) + self.assertEqual(['token-client-2'], resp2.headers['hdapr-api-token']) + + finally: + client1.close() + client2.close() + + @patch.object(DaprHealth, 'wait_until_ready') + async def test_multiple_async_clients_different_tokens(self, mock_health): + """Test that multiple async clients can use different tokens""" + # Mock health check to avoid connection issues + mock_health.return_value = None + + # Create two async clients with different tokens + client1 = DaprGrpcClientAsync(f'localhost:{self.grpc_port_1}', api_token='async-token-1') + client2 = DaprGrpcClientAsync(f'localhost:{self.grpc_port_2}', api_token='async-token-2') + + try: + # Make requests with both clients + resp1 = await client1.invoke_method( + app_id='app1', + method_name='test', + data=b'async-client1', + content_type='text/plain', + ) + + resp2 = await client2.invoke_method( + app_id='app2', + method_name='test', + data=b'async-client2', + content_type='text/plain', + ) + + # Verify each client used its own token + self.assertEqual(b'async-client1', resp1.data) + self.assertEqual(['async-token-1'], resp1.headers['hdapr-api-token']) + + self.assertEqual(b'async-client2', resp2.data) + self.assertEqual(['async-token-2'], resp2.headers['hdapr-api-token']) + + finally: + await client1.close() + await client2.close() + + @patch.object(DaprHealth, 'wait_until_ready') + @patch.object(settings, 'DAPR_API_TOKEN', 'global-default-token') + def test_mixing_explicit_and_global_tokens(self, mock_health): + """Test that clients with explicit tokens coexist with clients using global token""" + # Mock health check to avoid connection issues + mock_health.return_value = None + + # Client with explicit token + client_explicit = DaprGrpcClient( + f'localhost:{self.grpc_port_1}', api_token='explicit-token' + ) + # Client using global token + client_global = DaprGrpcClient(f'localhost:{self.grpc_port_2}') + + try: + resp_explicit = client_explicit.invoke_method( + app_id='app1', + method_name='test', + data=b'explicit', + content_type='text/plain', + ) + + resp_global = client_global.invoke_method( + app_id='app2', + method_name='test', + data=b'global', + content_type='text/plain', + ) + + # Verify explicit token client used its token + self.assertEqual(b'explicit', resp_explicit.data) + self.assertEqual(['explicit-token'], resp_explicit.headers['hdapr-api-token']) + + # Verify global token client used the global setting + self.assertEqual(b'global', resp_global.data) + self.assertEqual(['global-default-token'], resp_global.headers['hdapr-api-token']) + + finally: + client_explicit.close() + client_global.close() + + @patch.object(DaprHealth, 'wait_until_ready') + def test_client_isolation(self, mock_health): + """Test that modifying one client's token doesn't affect another""" + # Mock health check to avoid connection issues + mock_health.return_value = None + + # Create two clients with different tokens + client1 = DaprGrpcClient(f'localhost:{self.grpc_port_1}', api_token='isolated-token-1') + client2 = DaprGrpcClient(f'localhost:{self.grpc_port_2}', api_token='isolated-token-2') + + try: + # Make a request with client1 + resp1 = client1.invoke_method( + app_id='app1', method_name='test', data=b'test1', content_type='text/plain' + ) + + # Make a request with client2 + resp2 = client2.invoke_method( + app_id='app2', method_name='test', data=b'test2', content_type='text/plain' + ) + + # Make another request with client1 to verify it still uses its token + resp1_again = client1.invoke_method( + app_id='app1', method_name='test', data=b'test1_again', content_type='text/plain' + ) + + # Verify tokens are isolated + self.assertEqual(['isolated-token-1'], resp1.headers['hdapr-api-token']) + self.assertEqual(['isolated-token-2'], resp2.headers['hdapr-api-token']) + self.assertEqual(['isolated-token-1'], resp1_again.headers['hdapr-api-token']) + + finally: + client1.close() + client2.close() + + @patch.object(DaprHealth, 'wait_until_ready') + @patch.object(settings, 'DAPR_API_TOKEN', None) + def test_no_token_clients(self, mock_health): + """Test that clients without tokens work when no global token is set""" + # Mock health check to avoid connection issues + mock_health.return_value = None + + client = DaprGrpcClient(f'localhost:{self.grpc_port_1}') + + try: + resp = client.invoke_method( + app_id='app1', + method_name='test', + data=b'no-token', + content_type='text/plain', + ) + + # Verify no token was sent + self.assertNotIn('hdapr-api-token', resp.headers) + + finally: + client.close() + + +if __name__ == '__main__': + unittest.main() From 43b0b682181febd0f406f89c65e2c274353694db Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Thu, 23 Oct 2025 17:25:39 -0500 Subject: [PATCH 2/2] ruff Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- dapr/clients/http/dapr_actor_http_client.py | 4 +++- dapr/clients/http/dapr_invocation_http_client.py | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/dapr/clients/http/dapr_actor_http_client.py b/dapr/clients/http/dapr_actor_http_client.py index cfc3ff44..ac63672b 100644 --- a/dapr/clients/http/dapr_actor_http_client.py +++ b/dapr/clients/http/dapr_actor_http_client.py @@ -48,7 +48,9 @@ def __init__( api_token (str, optional): Dapr API token for authentication. If not provided, falls back to DAPR_API_TOKEN environment variable. """ - self._client = DaprHttpClient(message_serializer, timeout, headers_callback, retry_policy, api_token=api_token) + self._client = DaprHttpClient( + message_serializer, timeout, headers_callback, retry_policy, api_token=api_token + ) async def invoke_method( self, actor_type: str, actor_id: str, method: str, data: Optional[bytes] = None diff --git a/dapr/clients/http/dapr_invocation_http_client.py b/dapr/clients/http/dapr_invocation_http_client.py index b816fabd..043d292f 100644 --- a/dapr/clients/http/dapr_invocation_http_client.py +++ b/dapr/clients/http/dapr_invocation_http_client.py @@ -51,7 +51,11 @@ def __init__( falls back to DAPR_API_TOKEN environment variable. """ self._client = DaprHttpClient( - DefaultJSONSerializer(), timeout, headers_callback, retry_policy=retry_policy, api_token=api_token + DefaultJSONSerializer(), + timeout, + headers_callback, + retry_policy=retry_policy, + api_token=api_token, ) async def invoke_method_async(