From abb4ef9284a2359ad2efeeda2cc09bbf10bcac7c Mon Sep 17 00:00:00 2001 From: esubaalew Date: Thu, 26 Mar 2026 14:28:37 +0300 Subject: [PATCH] Fix unstable operation IDs for multi-method routes Make generated operation IDs deterministic when a route is registered with multiple HTTP methods. This avoids depending on set iteration order and adds a regression test to keep the behavior stable. --- fastapi/utils.py | 2 +- tests/test_generate_unique_id_function.py | 27 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/fastapi/utils.py b/fastapi/utils.py index 12eaa2bf08..9a8683e081 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -96,7 +96,7 @@ def generate_unique_id(route: "APIRoute") -> str: operation_id = f"{route.name}{route.path_format}" operation_id = re.sub(r"\W", "_", operation_id) assert route.methods - operation_id = f"{operation_id}_{list(route.methods)[0].lower()}" + operation_id = f"{operation_id}_{sorted(route.methods)[0].lower()}" return operation_id diff --git a/tests/test_generate_unique_id_function.py b/tests/test_generate_unique_id_function.py index c56e6d5794..97620d9ee5 100644 --- a/tests/test_generate_unique_id_function.py +++ b/tests/test_generate_unique_id_function.py @@ -1,8 +1,10 @@ import warnings +from types import SimpleNamespace from fastapi import APIRouter, FastAPI from fastapi.routing import APIRoute from fastapi.testclient import TestClient +from fastapi.utils import generate_unique_id from inline_snapshot import snapshot from pydantic import BaseModel @@ -29,6 +31,31 @@ class Message(BaseModel): description: str +class MethodsSet(set[str]): + def __init__(self, values: set[str], iteration_order: list[str]) -> None: + super().__init__(values) + self.iteration_order = iteration_order + + def __iter__(self): + return iter(self.iteration_order) + + +def test_generate_unique_id_with_multiple_methods_is_stable(): + route_a = SimpleNamespace( + name="handler", + path_format="/items", + methods=MethodsSet({"POST", "DELETE"}, ["POST", "DELETE"]), + ) + route_b = SimpleNamespace( + name="handler", + path_format="/items", + methods=MethodsSet({"POST", "DELETE"}, ["DELETE", "POST"]), + ) + + assert generate_unique_id(route_a) == "handler_items_delete" + assert generate_unique_id(route_a) == generate_unique_id(route_b) + + def test_top_level_generate_unique_id(): app = FastAPI(generate_unique_id_function=custom_generate_unique_id) router = APIRouter()