From 9ccef4328c50812ab12b0a5548d7b67b439c1822 Mon Sep 17 00:00:00 2001 From: 04cb <0x04cb@gmail.com> Date: Sat, 7 Mar 2026 13:37:53 +0800 Subject: [PATCH] Fix duplicate operation IDs for routes with multiple methods Fixes #13175. When a route is defined with multiple methods (e.g., methods=["POST", "DELETE"]), the generate_unique_id function was only using the first method to generate the operation_id. This caused all methods on the same route to have the same operation_id in the OpenAPI schema, triggering duplicate operation_id warnings. The fix modifies get_openapi_operation_metadata to generate unique operation_ids per method when a route has multiple methods, by appending the current method to the base unique_id. --- fastapi/openapi/utils.py | 11 ++++- tests/test_multiple_methods_unique_id.py | 52 ++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 tests/test_multiple_methods_unique_id.py diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index 828442559b..de99aa90ac 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -243,7 +243,16 @@ 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 + else: + # If the route has multiple methods, generate a unique operation_id + # using the current method to avoid duplicates + assert route.methods is not None + if len(route.methods) > 1: + 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 " diff --git a/tests/test_multiple_methods_unique_id.py b/tests/test_multiple_methods_unique_id.py new file mode 100644 index 0000000000..11be625a78 --- /dev/null +++ b/tests/test_multiple_methods_unique_id.py @@ -0,0 +1,52 @@ +""" +Test that routes with multiple methods get unique operation IDs. +This is a regression test for issue #13175. +""" +import warnings + +from fastapi import FastAPI +from fastapi.testclient import TestClient + + +def test_multiple_methods_generate_unique_operation_ids(): + """Test that add_api_route with multiple methods generates unique operation IDs.""" + app = FastAPI() + + def clear(): + return {"cleared": True} + + app.add_api_route("/clear", clear, methods=["POST", "DELETE"]) + + # Capture warnings to check for duplicate operation_id warnings + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + client = TestClient(app) + response = client.get("/openapi.json") + + # There should be no duplicate operation_id warnings + dup_warnings = [ + warning for warning in w + if "Duplicate Operation ID" in str(warning.message) + ] + assert len(dup_warnings) == 0, f"Expected no duplicate warnings, got: {dup_warnings}" + + openapi_schema = response.json() + + # Get the operation IDs for POST and DELETE + post_operation_id = openapi_schema["paths"]["/clear"]["post"]["operationId"] + delete_operation_id = openapi_schema["paths"]["/clear"]["delete"]["operationId"] + + # They should be different + assert post_operation_id != delete_operation_id, ( + f"POST and DELETE should have different operation IDs. " + f"Got POST: {post_operation_id}, DELETE: {delete_operation_id}" + ) + + # Verify the operation IDs contain the correct method suffix + assert post_operation_id.endswith("_post"), f"POST operation_id should end with '_post', got: {post_operation_id}" + assert delete_operation_id.endswith("_delete"), f"DELETE operation_id should end with '_delete', got: {delete_operation_id}" + + +if __name__ == "__main__": + test_multiple_methods_generate_unique_operation_ids() + print("Test passed!")