Browse Source

Fix duplicate operationId when route has multiple methods

When a route is registered with multiple HTTP methods via add_api_route(),
generate_unique_id() uses list(route.methods)[0] which is non-deterministic
(methods is a set) and the same for every method in the route. This causes
get_openapi_operation_metadata to assign identical operationIds to POST and
DELETE (or any other method pair), triggering "Duplicate Operation ID" warnings.

Two fixes:
- generate_unique_id: use sorted(route.methods)[0] so the fallback unique_id
  is deterministic for single-method routes
- get_openapi_operation_metadata: for multi-method routes without an explicit
  operation_id, derive the operationId from the route name/path plus the
  per-iteration method instead of reusing route.unique_id

Fixes #13175

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
pull/15114/head
Vishnu Kosuri 4 months ago
parent
commit
ea255c4cfe
  1. 9
      fastapi/openapi/utils.py
  2. 2
      fastapi/utils.py
  3. 29
      tests/test_generate_unique_id_function.py

9
fastapi/openapi/utils.py

@ -1,6 +1,7 @@
import copy
import http.client
import inspect
import re
import warnings
from collections.abc import Sequence
from typing import Any, Literal, cast
@ -243,7 +244,13 @@ 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 route.methods and len(route.methods) > 1:
base = re.sub(r"\W", "_", f"{route.name}{route.path_format}")
operation_id = f"{base}_{method.lower()}"
else:
operation_id = route.unique_id
if operation_id in operation_ids:
message = (
f"Duplicate Operation ID {operation_id} for function "

2
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

29
tests/test_generate_unique_id_function.py

@ -1697,3 +1697,32 @@ def test_warn_duplicate_operation_id():
]
assert len(duplicate_warnings) > 0
assert "Duplicate Operation ID" in str(duplicate_warnings[0].message)
def test_no_duplicate_operation_id_for_multi_method_route():
app = FastAPI()
def clear():
return "cleared" # pragma: nocover
app.router.add_api_route("/clear", clear, methods=["POST", "DELETE"])
client = TestClient(app)
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
response = client.get("/openapi.json")
duplicate_warnings = [
warning
for warning in w
if issubclass(warning.category, UserWarning)
and "Duplicate Operation ID" in str(warning.message)
]
assert len(duplicate_warnings) == 0
schema = response.json()
clear_path = schema["paths"]["/clear"]
assert "post" in clear_path
assert "delete" in clear_path
assert clear_path["post"]["operationId"] != clear_path["delete"]["operationId"]
assert clear_path["post"]["operationId"] == "clear_clear_post"
assert clear_path["delete"]["operationId"] == "clear_clear_delete"

Loading…
Cancel
Save