Skip to content

Commit fe874de

Browse files
chuenchen309claude
andcommitted
fix(openapi): use exact-match dedup in default_operation_id_creator
`default_operation_id_creator` builds an operationId by concatenating title-cased path components, skipping a component if it's already present via `component.title() not in components_namespace` — a substring check against the concatenated string, not an exact-match check against individual components. This causes false collisions: for path components `["users", "user"]`, "User" is a substring of the already-accumulated "Users", so the "user" component is silently dropped, producing the same operationId ("UsersGetHandler") as a route whose only component is `["users"]`. Two structurally different routes can end up with identical operationIds, which OpenAPI requires to be unique (test_routes_with_different_paths_should_generate_unique_operation_ids already encodes this invariant, just not for this substring case). Fix: track seen component titles in a set and check membership there instead of doing a substring check against the concatenated namespace string. This preserves the original intent (skip an exact-duplicate component, e.g. when a router prefix repeats a literal path segment) without falsely dropping components that merely share a substring. How verified: reproduced the collision directly against default_operation_id_creator with ["users"] vs ["users", "user"]) producing the same id; added a regression test (test_default_operation_id_creator_does_not_collide_on_component_substring) confirmed failing before the fix (AssertionError: 'UsersGetHandler' != 'UsersGetHandler'); applied the fix; confirmed all 23 tests in test_path_item.py and the full 262-test tests/unit/test_openapi/ suite pass. mypy clean, pre-commit clean (ruff, unasyncd, typos). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 4a43322 commit fe874de

2 files changed

Lines changed: 19 additions & 2 deletions

File tree

litestar/_openapi/utils.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,11 @@ def default_operation_id_creator(
3939
)
4040

4141
components_namespace = ""
42+
seen_components: set[str] = set()
4243
for component in (c.name if isinstance(c, PathParameterDefinition) else c for c in path_components):
43-
if component.title() not in components_namespace:
44-
components_namespace += component.title()
44+
title = component.title()
45+
if title not in seen_components:
46+
seen_components.add(title)
47+
components_namespace += title
4548

4649
return SEPARATORS_CLEANUP_PATTERN.sub("", components_namespace + handler_namespace)

tests/unit/test_openapi/test_path_item.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,20 @@ def test_routes_with_different_paths_should_generate_unique_operation_ids(
129129
assert schema_v1.get.operation_id != schema_v2.get.operation_id
130130

131131

132+
def test_default_operation_id_creator_does_not_collide_on_component_substring() -> None:
133+
handler = MagicMock()
134+
handler.handler_name = "get_handler"
135+
handler.http_methods = ["GET"]
136+
137+
# "user" is a substring of the already-accumulated "Users", but it is a distinct,
138+
# additional path component and must not be dropped from the generated id.
139+
id_for_users = default_operation_id_creator(handler, "GET", ["users"])
140+
id_for_users_user = default_operation_id_creator(handler, "GET", ["users", "user"])
141+
142+
assert id_for_users != id_for_users_user
143+
assert id_for_users_user == "UsersUserGetHandler"
144+
145+
132146
def test_create_path_item_use_handler_docstring_false(
133147
http_route: HTTPRoute, create_factory: CreateFactoryFixture
134148
) -> None:

0 commit comments

Comments
 (0)