From 1290ca451b62d57433a7496a981dd323b7ff85e4 Mon Sep 17 00:00:00 2001 From: Kayvan Zahiri Date: Mon, 21 Sep 2026 11:06:01 -0700 Subject: [PATCH 1/2] fix: accept absolute unix socket paths in GrpcEndpoint unix:///path/to.sock and unix:/path/to.sock put the socket path in the parsed URL's path, which the path check rejected, so WorkflowRuntime, DaprWorkflowClient and DaprClient all failed on them. Allow a path for the unix scheme and build the hostname from netloc + path so the filename's case is kept. Signed-off-by: Kayvan Zahiri --- dapr/conf/helpers.py | 11 +++++- tests/conf/helpers_test.py | 38 +++++++++++++++++++++ tests/ext/workflow/test_workflow_runtime.py | 28 +++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/dapr/conf/helpers.py b/dapr/conf/helpers.py index d2d187628..4b63ff829 100644 --- a/dapr/conf/helpers.py +++ b/dapr/conf/helpers.py @@ -55,6 +55,11 @@ def scheme(self) -> str: return self._scheme def _set_hostname(self): + if self._is_unix_socket_path(): + # netloc + path keeps the filesystem path's case; parsed_url.hostname lowercases it. + self._hostname = f'{self._parsed_url.netloc}{self._parsed_url.path}' + return + if self._parsed_url.hostname is None: self._hostname = URIParseConfig.DEFAULT_HOSTNAME return @@ -171,8 +176,12 @@ def _set_tls(self): def tls(self) -> bool: return self._tls + def _is_unix_socket_path(self) -> bool: + """Whether this is a unix socket whose file path urlparse put in ``path``.""" + return self._parsed_url.scheme == 'unix' and bool(self._parsed_url.path) + def _validate_path_and_query(self) -> None: - if self._parsed_url.path: + if self._parsed_url.path and not self._is_unix_socket_path(): raise ValueError( f"paths are not supported for gRPC endpoints: '{self._parsed_url.path}'" ) diff --git a/tests/conf/helpers_test.py b/tests/conf/helpers_test.py index d09eb3d9d..c49ca9cc5 100644 --- a/tests/conf/helpers_test.py +++ b/tests/conf/helpers_test.py @@ -236,6 +236,44 @@ def test_parse_grpc_endpoint(self): 'port': '', 'endpoint': 'unix://my.sock', }, + # Unix sockets with a filesystem path, which urlparse puts in the path component + { + 'url': 'unix:///tmp/dapr.sock', + 'error': False, + 'secure': False, + 'scheme': 'unix', + 'host': '/tmp/dapr.sock', + 'port': '', + 'endpoint': 'unix:///tmp/dapr.sock', + }, + { + 'url': 'unix:///tmp/dapr.sock?tls=true', + 'error': False, + 'secure': True, + 'scheme': 'unix', + 'host': '/tmp/dapr.sock', + 'port': '', + 'endpoint': 'unix:///tmp/dapr.sock', + }, + { + 'url': 'unix:/tmp/dapr.sock', + 'error': False, + 'secure': False, + 'scheme': 'unix', + 'host': '/tmp/dapr.sock', + 'port': '', + 'endpoint': 'unix:/tmp/dapr.sock', + }, + { + 'url': 'unix:///private/tmp/dapr-sockets/Dapr-MyApp-grpc.socket', + 'error': False, + 'secure': False, + 'scheme': 'unix', + 'host': '/private/tmp/dapr-sockets/Dapr-MyApp-grpc.socket', + 'port': '', + 'endpoint': 'unix:///private/tmp/dapr-sockets/Dapr-MyApp-grpc.socket', + }, + {'url': 'unix:///tmp/dapr.sock?abc=1', 'error': True}, # Unix abstract sockets { 'url': 'unix-abstract:my.sock', diff --git a/tests/ext/workflow/test_workflow_runtime.py b/tests/ext/workflow/test_workflow_runtime.py index 35bb9e872..e14a3b1f2 100644 --- a/tests/ext/workflow/test_workflow_runtime.py +++ b/tests/ext/workflow/test_workflow_runtime.py @@ -106,6 +106,34 @@ def test_timeout_interceptor_preserves_custom_interceptor_order(self): self.assertIsInstance(interceptors[2], DaprClientTimeoutInterceptor) +class WorkflowRuntimeUnixSocketTest(unittest.TestCase): + """An absolute unix socket in DAPR_GRPC_ENDPOINT must reach the worker as a gRPC target.""" + + def setUp(self): + self._registry_patch = mock.patch( + 'dapr.ext.workflow._durabletask.worker._Registry', + return_value=FakeTaskHubGrpcWorker(), + ) + self._registry_patch.start() + + def tearDown(self): + mock.patch.stopall() + + def test_absolute_unix_socket_endpoint_is_forwarded(self): + socket_endpoint = 'unix:///private/tmp/dapr-sockets/dapr-myapp-grpc.socket' + with ( + mock.patch.object(settings, 'DAPR_GRPC_ENDPOINT', socket_endpoint), + mock.patch( + 'dapr.ext.workflow._durabletask.worker.TaskHubGrpcWorker' + ) as mock_worker_cls, + ): + WorkflowRuntime() + call_kwargs = mock_worker_cls.call_args[1] + + self.assertEqual(call_kwargs['host_address'], socket_endpoint) + self.assertFalse(call_kwargs['secure_channel']) + + class WorkflowRuntimeStatefulHistoryTest(unittest.TestCase): """The stateful-history options must reach the worker, not stop at the public API.""" From b563a2a492fca2622bd9c0447ae6643da604ccc3 Mon Sep 17 00:00:00 2001 From: Kayvan Zahiri Date: Fri, 25 Sep 2026 09:40:21 -0700 Subject: [PATCH 2/2] fix: reject unix:// endpoints with an authority, keep socket name case gRPC needs an absolute path after unix://, so unix://my.sock and unix://rel/dir/x.sock can never connect; raise ValueError instead of hanging until the timeout. Build the hostname from netloc + path for every unix endpoint so unix:My.sock keeps its case. Signed-off-by: Kayvan Zahiri --- dapr/conf/helpers.py | 7 ++++++- tests/conf/helpers_test.py | 20 ++++++++++++-------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/dapr/conf/helpers.py b/dapr/conf/helpers.py index 4b63ff829..a743b8dbd 100644 --- a/dapr/conf/helpers.py +++ b/dapr/conf/helpers.py @@ -55,7 +55,7 @@ def scheme(self) -> str: return self._scheme def _set_hostname(self): - if self._is_unix_socket_path(): + if self._parsed_url.scheme == 'unix': # netloc + path keeps the filesystem path's case; parsed_url.hostname lowercases it. self._hostname = f'{self._parsed_url.netloc}{self._parsed_url.path}' return @@ -181,6 +181,11 @@ def _is_unix_socket_path(self) -> bool: return self._parsed_url.scheme == 'unix' and bool(self._parsed_url.path) def _validate_path_and_query(self) -> None: + if self._url.startswith('unix://') and self._parsed_url.netloc: + raise ValueError( + f"unix:// endpoints require an absolute path: '{self._url}'; " + 'use unix:relative or unix:///absolute' + ) if self._parsed_url.path and not self._is_unix_socket_path(): raise ValueError( f"paths are not supported for gRPC endpoints: '{self._parsed_url.path}'" diff --git a/tests/conf/helpers_test.py b/tests/conf/helpers_test.py index c49ca9cc5..a80d76360 100644 --- a/tests/conf/helpers_test.py +++ b/tests/conf/helpers_test.py @@ -217,24 +217,28 @@ def test_parse_grpc_endpoint(self): 'port': '', 'endpoint': 'unix:my.sock', }, - # Unix sockets with absolute path + # gRPC needs an absolute path after unix://, so an authority can never connect + {'url': 'unix://my.sock', 'error': True}, + {'url': 'unix://my.sock?tls=true', 'error': True}, + {'url': 'unix://relative/dir/x.sock', 'error': True}, + # Relative socket names keep their case { - 'url': 'unix://my.sock', + 'url': 'unix:My.sock', 'error': False, 'secure': False, 'scheme': 'unix', - 'host': 'my.sock', + 'host': 'My.sock', 'port': '', - 'endpoint': 'unix://my.sock', + 'endpoint': 'unix:My.sock', }, { - 'url': 'unix://my.sock?tls=true', + 'url': 'unix:run/My.sock', 'error': False, - 'secure': True, + 'secure': False, 'scheme': 'unix', - 'host': 'my.sock', + 'host': 'run/My.sock', 'port': '', - 'endpoint': 'unix://my.sock', + 'endpoint': 'unix:run/My.sock', }, # Unix sockets with a filesystem path, which urlparse puts in the path component {