Browse Source

fix: unique operation IDs for routes with multiple methods (#13175)

generate_unique_id used list(route.methods)[0] so all methods on a route
shared the same operation_id. For multi-method routes, use method-specific
IDs in get_openapi_operation_metadata.

Made-with: Cursor
pull/15140/head
aayushbaluni 4 months ago
parent
commit
135010c195
  1. 9
      fastapi/openapi/utils.py
  2. 21
      tests/test_generate_unique_id_function.py

9
fastapi/openapi/utils.py

@ -242,7 +242,14 @@ 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:
# Multi-method routes: unique_id uses first method only; use method-specific id
base = route.unique_id.rsplit("_", 1)[0]
operation_id = f"{base}_{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}"

21
tests/test_generate_unique_id_function.py

@ -1697,3 +1697,24 @@ def test_warn_duplicate_operation_id():
]
assert len(duplicate_warnings) > 0
assert "Duplicate Operation ID" in str(duplicate_warnings[0].message)
def test_multiple_methods_unique_operation_id():
"""Regression test for #13175: routes with multiple methods get unique operation IDs."""
app = FastAPI()
def clear():
return {"ok": True}
app.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")
assert len([x for x in w if "Duplicate Operation ID" in str(getattr(x, "message", ""))]) == 0
paths = response.json()["paths"]["/clear"]
post_id = paths["post"]["operationId"]
delete_id = paths["delete"]["operationId"]
assert post_id != delete_id
assert post_id.endswith("_post")
assert delete_id.endswith("_delete")

Loading…
Cancel
Save