diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index 8f1852b0cc..311cd2165e 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -242,7 +242,20 @@ def get_openapi_operation_metadata( operation["summary"] = generate_operation_summary(route=route, method=method) if route.description: operation["description"] = route.description - operation_id = route.operation_id or route.unique_id + if route.operation_id: + operation_id = route.operation_id + elif len(route.methods) > 1: + # When a route handles multiple methods, the default unique_id is based on + # the first method only (via generate_unique_id). Append the actual method + # to avoid duplicate operation IDs across methods. See #13175. + base = route.unique_id + # Remove the trailing _ suffix that generate_unique_id appends + first_method = f"_{list(route.methods)[0].lower()}" + if base.endswith(first_method): + base = base[: -len(first_method)] + operation_id = f"{base}_{method.lower()}" + else: + operation_id = route.unique_id if operation_id in operation_ids: endpoint_name = getattr(route.endpoint, "__name__", "") message = f"Duplicate Operation ID {operation_id} for function {endpoint_name}" diff --git a/tests/test_generate_unique_id_function.py b/tests/test_generate_unique_id_function.py index c56e6d5794..3a9ddf8386 100644 --- a/tests/test_generate_unique_id_function.py +++ b/tests/test_generate_unique_id_function.py @@ -1697,3 +1697,30 @@ def test_warn_duplicate_operation_id(): ] assert len(duplicate_warnings) > 0 assert "Duplicate Operation ID" in str(duplicate_warnings[0].message) + + +def test_unique_operation_id_for_multi_method_route(): + """A route with multiple methods should get unique operation IDs per method. + + Regression test for https://github.com/fastapi/fastapi/issues/13175 + """ + app = FastAPI() + + def handler(): + return {"msg": "ok"} + + app.add_api_route("/clear", handler, methods=["POST", "DELETE"]) + + client = TestClient(app) + response = client.get("/openapi.json") + data = response.json() + paths = data["paths"] + + op_ids = [] + for methods in paths.values(): + for details in methods.values(): + op_ids.append(details["operationId"]) + + assert len(op_ids) == 2, f"Expected 2 operations, got {len(op_ids)}" + assert op_ids[0] != op_ids[1], f"Duplicate operation IDs: {op_ids}" + assert set(op_ids) == {"handler_clear_post", "handler_clear_delete"}