Repository: litestar-org/litestar

Issue:
Bug: OpenAPI schema generation fails due to same operation IDs
### Description

If two routes with the same path, but different methods are defined then the OpenAPI generation fails due to both of them having the same value for operation ID. After running `git bisect`, #2805 seems to have introduced this.

### URL to code causing the issue

_No response_

### MCVE

```python
from litestar import get, post
from litestar.app import Litestar
from litestar.testing import create_test_client


@post("/")
async def post_handler() -> None:
    ...


@get("/")
async def get_handler() -> None:
    ...


with create_test_client([post_handler, get_handler]) as client:
    response = client.get("/schema/openapi.json")

    assert response.status_code == 200
```


### Steps to reproduce

_No response_

### Screenshots

_No response_

### Logs

_No response_

### Litestar Version

HEAD

### Platform

- [ ] Linux
- [ ] Mac
- [ ] Windows
- [ ] Other (Please specify in the description above)

<!-- POLAR PLEDGE BADGE START -->
---
> [!NOTE]  
> While we are open for sponsoring on [GitHub Sponsors](https://github.com/sponsors/litestar-org/) and 
> [OpenCollective](https://opencollective.com/litestar), we also utilize [Polar.sh](https://polar.sh/) to engage in pledge-based sponsorship.
>
> Check out all issues funded or available for funding [on our Polar.sh dashboard](https://polar.sh/litestar-org)
> * If you would like to see an issue prioritized, make a pledge towards it!
> * We receive the pledge once the issue is completed & verified
> * This, along with engagement in the community, helps us know which features are a priority to our users.

<a href="https://polar.sh/litestar-org/litestar/issues/2863">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/api/github/litestar-org/litestar/issues/2863/pledge.svg?darkmode=1">
  <img alt="Fund with Polar" src="https://polar.sh/api/github/litestar-org/litestar/issues/2863/pledge.svg">
</picture>
</a>
<!-- POLAR PLEDGE BADGE END -->



Discussion:
The reason this is happening is because in the `receive_route` the routes are added to the `included_routes` of the plugin as seen [here](https://github.com/litestar-org/litestar/blob/b02e65e675d8a29d86be90be79b997c79588823c/litestar/_openapi/plugin.py#L60-L67).

However, this causes the creation of the path item for the same path multiple times since a single path may have multiple route handlers. When this happens, an operation ID is created for a route handler which was already processed and hence we get an error.

Resolving patch:
diff --git a/litestar/_openapi/plugin.py b/litestar/_openapi/plugin.py
--- a/litestar/_openapi/plugin.py
+++ b/litestar/_openapi/plugin.py
@@ -27,7 +27,7 @@ class OpenAPIPlugin(InitPluginProtocol, ReceiveRoutePlugin):
 
     def __init__(self, app: Litestar) -> None:
         self.app = app
-        self.included_routes: list[HTTPRoute] = []
+        self.included_routes: dict[str, HTTPRoute] = {}
         self._openapi_config: OpenAPIConfig | None = None
         self._openapi_schema: OpenAPI | None = None
 
@@ -35,7 +35,8 @@ def _build_openapi_schema(self) -> OpenAPI:
         openapi = self.openapi_config.to_openapi_schema()
         context = OpenAPIContext(openapi_config=self.openapi_config, plugins=self.app.plugins.openapi)
         openapi.paths = {
-            route.path_format or "/": create_path_item_for_route(context, route) for route in self.included_routes
+            route.path_format or "/": create_path_item_for_route(context, route)
+            for route in self.included_routes.values()
         }
         openapi.components.schemas = context.schema_registry.generate_components_schemas()
         return openapi
@@ -64,4 +65,4 @@ def receive_route(self, route: BaseRoute) -> None:
         if any(route_handler.resolve_include_in_schema() for route_handler, _ in route.route_handler_map.values()):
             # Force recompute the schema if a new route is added
             self._openapi_schema = None
-            self.included_routes.append(route)
+            self.included_routes[route.path] = route


Test patch:
diff --git a/tests/unit/test_openapi/test_integration.py b/tests/unit/test_openapi/test_integration.py
--- a/tests/unit/test_openapi/test_integration.py
+++ b/tests/unit/test_openapi/test_integration.py
@@ -352,3 +352,22 @@ def handler_b() -> module_b.Model:  # type: ignore[name-defined]
         f"{module_b.__name__}_Model",
     }
     # TODO: expand this test to cover more cases
+
+
+def test_multiple_handlers_for_same_route() -> None:
+    @post("/", sync_to_thread=False)
+    def post_handler() -> None:
+        ...
+
+    @get("/", sync_to_thread=False)
+    def get_handler() -> None:
+        ...
+
+    app = Litestar([get_handler, post_handler])
+    openapi_plugin = app.plugins.get(OpenAPIPlugin)
+    openapi = openapi_plugin.provide_openapi()
+
+    assert openapi.paths is not None
+    path_item = openapi.paths["/"]
+    assert path_item.get is not None
+    assert path_item.post is not None