Browse Source

🐛 Fix duplicate operationId when using add_api_route() with multiple HTTP methods

pull/15321/head
ShirGanon 3 months ago
parent
commit
33556e8863
  1. 11
      fastapi/openapi/utils.py
  2. 81
      tests/test_generate_unique_id_function.py

11
fastapi/openapi/utils.py

@ -1,6 +1,7 @@
import copy import copy
import http.client import http.client
import inspect import inspect
import re
import warnings import warnings
from collections.abc import Sequence from collections.abc import Sequence
from typing import Any, Literal, cast from typing import Any, Literal, cast
@ -242,7 +243,15 @@ def get_openapi_operation_metadata(
operation["summary"] = generate_operation_summary(route=route, method=method) operation["summary"] = generate_operation_summary(route=route, method=method)
if route.description: if route.description:
operation["description"] = 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:
# Multi-method route without explicit operation_id: generate per-method ID
# to avoid duplicate operationIds in the OpenAPI schema
operation_id = re.sub(r"\W", "_", f"{route.name}{route.path_format}")
operation_id = f"{operation_id}_{method.lower()}"
else:
operation_id = route.unique_id
if operation_id in operation_ids: if operation_id in operation_ids:
endpoint_name = getattr(route.endpoint, "__name__", "<unnamed_endpoint>") endpoint_name = getattr(route.endpoint, "__name__", "<unnamed_endpoint>")
message = f"Duplicate Operation ID {operation_id} for function {endpoint_name}" message = f"Duplicate Operation ID {operation_id} for function {endpoint_name}"

81
tests/test_generate_unique_id_function.py

@ -1697,3 +1697,84 @@ def test_warn_duplicate_operation_id():
] ]
assert len(duplicate_warnings) > 0 assert len(duplicate_warnings) > 0
assert "Duplicate Operation ID" in str(duplicate_warnings[0].message) assert "Duplicate Operation ID" in str(duplicate_warnings[0].message)
def test_multi_method_route_no_duplicate_operation_id():
app = FastAPI()
router = APIRouter()
def multi_handler(item_id: int):
return {"id": item_id} # pragma: nocover
router.add_api_route("/items/{item_id}", multi_handler, methods=["GET", "DELETE"])
app.include_router(router)
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 duplicate_warnings == []
schema = response.json()
path_ops = schema["paths"]["/items/{item_id}"]
get_id = path_ops["get"]["operationId"]
delete_id = path_ops["delete"]["operationId"]
assert get_id != delete_id
assert get_id.endswith("_get")
assert delete_id.endswith("_delete")
def test_single_method_route_operation_id_unchanged():
app = FastAPI()
def my_handler():
return {} # pragma: nocover
app.add_api_route("/ping", my_handler, methods=["GET"])
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 duplicate_warnings == []
schema = response.json()
assert schema["paths"]["/ping"]["get"]["operationId"] == "my_handler_ping_get"
def test_three_method_route_distinct_operation_ids():
app = FastAPI()
def handler():
return {} # pragma: nocover
app.add_api_route("/multi", handler, methods=["GET", "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 duplicate_warnings == []
schema = response.json()
path_ops = schema["paths"]["/multi"]
op_ids = [path_ops[m]["operationId"] for m in ("get", "post", "delete")]
assert len(set(op_ids)) == 3, f"Expected 3 unique IDs, got: {op_ids}"

Loading…
Cancel
Save