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 http.client
import inspect
import re
import warnings
from collections.abc import Sequence
from typing import Any, Literal, cast
@ -242,7 +243,15 @@ 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:
# 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:
endpoint_name = getattr(route.endpoint, "__name__", "<unnamed_endpoint>")
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 "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