From 3221550928573c696309c92df3f32a32d1210241 Mon Sep 17 00:00:00 2001 From: Raphael Fakhri <153192858+RaphaelFakhri@users.noreply.github.com> Date: Tue, 29 Sep 2026 18:40:07 +0000 Subject: [PATCH] fix: Reject duplicate handler names at registration --- python/restate/object.py | 4 ++- python/restate/service.py | 4 ++- python/restate/workflow.py | 4 ++- tests/duplicate_handlers.py | 54 +++++++++++++++++++++++++++++++++++++ 4 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 tests/duplicate_handlers.py diff --git a/python/restate/object.py b/python/restate/object.py index 14d5ef5..1de7c5b 100644 --- a/python/restate/object.py +++ b/python/restate/object.py @@ -175,7 +175,7 @@ def handler( Callable: The decorated function. Raises: - ValueError: If the handler name is not provided. + ValueError: If the handler name is not provided, or a handler with the same name is already registered. Example: @service.handler() @@ -215,6 +215,8 @@ def wrapped(*args, **kwargs): invocation_retry_policy, combined_context_managers, ) + if handler.name in self.handlers: + raise ValueError(f"Handler {handler.name} already exists in {self.name}") self.handlers[handler.name] = handler return wrapped diff --git a/python/restate/service.py b/python/restate/service.py index cecd90f..59c034a 100644 --- a/python/restate/service.py +++ b/python/restate/service.py @@ -163,7 +163,7 @@ def handler( Callable: The decorated function. Raises: - ValueError: If the handler name is not provided. + ValueError: If the handler name is not provided, or a handler with the same name is already registered. Example: @service.handler() @@ -206,6 +206,8 @@ def wrapped(*args, **kwargs): invocation_retry_policy, combined_context_managers, ) + if handler.name in self.handlers: + raise ValueError(f"Handler {handler.name} already exists in {self.name}") self.handlers[handler.name] = handler return wrapped diff --git a/python/restate/workflow.py b/python/restate/workflow.py index b87a996..07b3572 100644 --- a/python/restate/workflow.py +++ b/python/restate/workflow.py @@ -342,7 +342,7 @@ def _add_handler( Callable: The decorated function. Raises: - ValueError: If the handler name is not provided. + ValueError: If the handler name is not provided, or a handler with the same name is already registered. Example: @service.handler() @@ -383,6 +383,8 @@ def wrapped(*args, **kwargs): invocation_retry_policy=invocation_retry_policy, context_managers=combined_context_managers, ) + if handler.name in self.handlers: + raise ValueError(f"Handler {handler.name} already exists in {self.name}") self.handlers[handler.name] = handler return wrapped diff --git a/tests/duplicate_handlers.py b/tests/duplicate_handlers.py new file mode 100644 index 0000000..9bd6754 --- /dev/null +++ b/tests/duplicate_handlers.py @@ -0,0 +1,54 @@ +# +# Copyright (c) 2023-2026 - Restate Software, Inc., Restate GmbH +# +# This file is part of the Restate SDK for Python, +# which is released under the MIT license. +# +# You can find a copy of the license in file LICENSE in the root +# directory of this repository or package, or at +# https://github.com/restatedev/sdk-typescript/blob/main/LICENSE +# +import pytest +import restate + + +def test_service_rejects_duplicate_handler_name(): + service = restate.Service("Greeter") + + @service.handler("greet") + async def first(ctx: restate.Context) -> str: + return "first" + + with pytest.raises(ValueError, match="Handler greet already exists in Greeter"): + + @service.handler("greet") + async def second(ctx: restate.Context) -> str: + return "second" + + +def test_virtual_object_rejects_duplicate_handler_name(): + obj = restate.VirtualObject("Counter") + + @obj.handler("add") + async def first(ctx: restate.ObjectContext) -> int: + return 1 + + with pytest.raises(ValueError, match="Handler add already exists in Counter"): + + @obj.handler("add") + async def second(ctx: restate.ObjectContext) -> int: + return 2 + + +def test_workflow_rejects_duplicate_handler_name(): + workflow = restate.Workflow("Signup") + + @workflow.main("run") + async def run(ctx: restate.WorkflowContext) -> str: + return "run" + + with pytest.raises(ValueError, match="Handler run already exists in Signup"): + + @workflow.handler("run") + async def other(ctx: restate.WorkflowSharedContext) -> str: + return "other"