Browse Source

Fix duplicate OperationID when adding route with multiple methods

When a route handles multiple HTTP methods (e.g. methods=["POST", "DELETE"]),
the operation ID was generated once from the first method in the set, causing
duplicate operation IDs in the OpenAPI schema and triggering warnings.

Now, for routes with multiple methods, the current method is appended to the
unique_id to ensure each method gets a distinct operation ID.

Closes #13175
pull/15009/head
Anandesh Sharma 5 months ago
parent
commit
a8f189ad7a
  1. 9
      fastapi/openapi/utils.py
  2. 71
      tests/test_duplicate_operation_id_multiple_methods.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 is not None and len(route.methods) > 1:
# When a route handles multiple HTTP methods, append the current
# method to create unique operation IDs per method
operation_id = f"{route.unique_id}_{method.lower()}"
else:
operation_id = route.unique_id
if operation_id in operation_ids:
message = (
f"Duplicate Operation ID {operation_id} for function "

71
tests/test_duplicate_operation_id_multiple_methods.py

@ -0,0 +1,71 @@
import warnings
from fastapi import FastAPI
from fastapi.testclient import TestClient
def test_no_duplicate_operation_id_with_multiple_methods():
"""Ensure routes with multiple methods get unique operation IDs."""
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
app = FastAPI()
def handler():
return {"status": "ok"} # pragma: nocover
app.add_api_route("/items", handler, methods=["POST", "DELETE"])
client = TestClient(app)
response = client.get("/openapi.json")
assert response.status_code == 200
schema = response.json()
post_op = schema["paths"]["/items"]["post"]
delete_op = schema["paths"]["/items"]["delete"]
assert post_op["operationId"] != delete_op["operationId"]
assert "post" in post_op["operationId"]
assert "delete" in delete_op["operationId"]
dup_warnings = [x for x in w if "Duplicate" in str(x.message)]
assert len(dup_warnings) == 0
def test_single_method_route_operation_id_unchanged():
"""Ensure single-method routes still generate the same operation IDs."""
app = FastAPI()
@app.get("/items")
def get_items():
return [] # pragma: nocover
client = TestClient(app)
response = client.get("/openapi.json")
schema = response.json()
get_op = schema["paths"]["/items"]["get"]
assert get_op["operationId"] == "get_items_items_get"
def test_three_methods_unique_operation_ids():
"""Ensure routes with three methods all get unique operation IDs."""
app = FastAPI()
def handler():
return {"status": "ok"} # pragma: nocover
app.add_api_route("/items", handler, methods=["GET", "POST", "DELETE"])
client = TestClient(app)
response = client.get("/openapi.json")
schema = response.json()
get_op = schema["paths"]["/items"]["get"]
post_op = schema["paths"]["/items"]["post"]
delete_op = schema["paths"]["/items"]["delete"]
assert "get" in get_op["operationId"]
assert "post" in post_op["operationId"]
assert "delete" in delete_op["operationId"]
assert len({get_op["operationId"], post_op["operationId"], delete_op["operationId"]}) == 3
Loading…
Cancel
Save