Repository: litestar-org/litestar

Issue:
Bug: Routes with duplicate parameter names are not shown in OpenAPI rendering
### Description

OpenAPI schema fails to generate if there's 2 parameters present with identical name, route is subsequently removed from OpenAPI schema.

### URL to code causing the issue

_No response_

### MCVE

```python
from typing import Annotated

from litestar import Litestar, post
from litestar.params import Parameter


@post("/test")
async def route(
    name: Annotated[str | None, Parameter(cookie="name")] = None,
    name_header: Annotated[str | None, Parameter(header="name")] = None,
) -> str:
    return name or name_header or ""


app = Litestar(
    route_handlers=[route],
)
```


### Steps to reproduce

```bash
1. Launch uvicorn server `uvicorn app:app`
2. Go to http://127.0.0.1:8000/schema/swagger
3. See error
```


### Screenshots

_No response_

### Logs

```bash
$ uvicorn app:app
INFO:     Started server process [8376]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     127.0.0.1:54807 - "GET /schema/swagger HTTP/1.1" 500 Internal Server Error
INFO:     127.0.0.1:54807 - "GET /schema/swagger HTTP/1.1" 200 OK
```


### Litestar Version

2.3.2

### Platform

- [ ] Linux
- [ ] Mac
- [X] Windows
- [ ] Other (Please specify in the description above)



Discussion:
Changed the title to be more descriptive of the actual issue. The routes continue to work, it's just that they are not shown on any OpenAPI renderers (swagger, elements etc) after the first load which raises the following exception (not attaching the stack trace).

```
500: OpenAPI schema generation for handler `app.route` detected multiple parameters named 'name' with different types.
```

Related PR (???) - https://github.com/litestar-org/litestar/pull/129
I should have originally attached stacktrace, but I'm fairly new to litestar and didn't know how to configure debug/logging options 🙂
Also on first request `openapi.json` returns status code 500 🤔 
> I should have originally attached stacktrace, but I'm fairly new to litestar and didn't know how to configure debug/logging options 🙂

No worries 🙂 I just do `app = Litestar(route_handlers=[route], debug=True)` and run the app.


> Also on first request `openapi.json` returns status code 500 🤔

This is not limited to your issue per se, most issues in the schema generation exhibit the same behavior, it sort of gets "cached" which is why you only see the error on the first try and only the "non erroneous" working schema from the second.

Resolving patch:
diff --git a/litestar/_openapi/parameters.py b/litestar/_openapi/parameters.py
--- a/litestar/_openapi/parameters.py
+++ b/litestar/_openapi/parameters.py
@@ -38,7 +38,7 @@ def __init__(self, route_handler: BaseRouteHandler) -> None:
             route_handler: Associated route handler
         """
         self.route_handler = route_handler
-        self._parameters: dict[str, Parameter] = {}
+        self._parameters: dict[tuple[str, str], Parameter] = {}
 
     def add(self, parameter: Parameter) -> None:
         """Add a ``Parameter`` to the collection.
@@ -50,18 +50,18 @@ def add(self, parameter: Parameter) -> None:
         ``ImproperlyConfiguredException``.
         """
 
-        if parameter.name not in self._parameters:
+        if (parameter.name, parameter.param_in) not in self._parameters:
             # because we are defining routes as unique per path, we have to handle here a situation when there is an optional
             # path parameter. e.g. get(path=["/", "/{param:str}"]). When parsing the parameter for path, the route handler
             # would still have a kwarg called param:
             # def handler(param: str | None) -> ...
             if parameter.param_in != ParamType.QUERY or all(
-                "{" + parameter.name + ":" not in path for path in self.route_handler.paths
+                f"{{{parameter.name}:" not in path for path in self.route_handler.paths
             ):
-                self._parameters[parameter.name] = parameter
+                self._parameters[(parameter.name, parameter.param_in)] = parameter
             return
 
-        pre_existing = self._parameters[parameter.name]
+        pre_existing = self._parameters[(parameter.name, parameter.param_in)]
         if parameter == pre_existing:
             return
 
@@ -206,13 +206,13 @@ def create_parameter_for_handler(
     dependency_providers = route_handler.resolve_dependencies()
     layered_parameters = route_handler.resolve_layered_parameters()
 
-    unique_handler_fields = tuple(
+    unique_handler_fields = (
         (k, v) for k, v in handler_fields.items() if k not in RESERVED_KWARGS and k not in layered_parameters
     )
-    unique_layered_fields = tuple(
+    unique_layered_fields = (
         (k, v) for k, v in layered_parameters.items() if k not in RESERVED_KWARGS and k not in handler_fields
     )
-    intersection_fields = tuple(
+    intersection_fields = (
         (k, v) for k, v in handler_fields.items() if k not in RESERVED_KWARGS and k in layered_parameters
     )
 


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
@@ -1,17 +1,19 @@
 from __future__ import annotations
 
 from dataclasses import dataclass
-from typing import Generic, Optional, TypeVar
+from typing import Generic, Optional, TypeVar, cast
 
 import msgspec
 import pytest
 import yaml
 from typing_extensions import Annotated
 
-from litestar import Controller, get, post
+from litestar import Controller, Litestar, get, post
 from litestar.app import DEFAULT_OPENAPI_CONFIG
 from litestar.enums import MediaType, OpenAPIMediaType, ParamType
 from litestar.openapi import OpenAPIConfig, OpenAPIController
+from litestar.openapi.spec import Parameter as OpenAPIParameter
+from litestar.params import Parameter
 from litestar.serialization.msgspec_hooks import decode_json, encode_json, get_serializer
 from litestar.status_codes import HTTP_200_OK, HTTP_404_NOT_FOUND
 from litestar.testing import create_test_client
@@ -291,3 +293,32 @@ def handler_foo_int() -> Foo[int]:
                 }
             },
         }
+
+
+def test_allow_multiple_parameters_with_same_name_but_different_location() -> None:
+    """Test that we can support params with the same name if they are in different locations, e.g., cookie and header.
+
+    https://github.com/litestar-org/litestar/issues/2662
+    """
+
+    @post("/test")
+    async def route(
+        name: Annotated[Optional[str], Parameter(cookie="name")] = None,  # noqa: UP007
+        name_header: Annotated[Optional[str], Parameter(header="name")] = None,  # noqa: UP007
+    ) -> str:
+        return name or name_header or ""
+
+    app = Litestar(route_handlers=[route], debug=True)
+    assert app.openapi_schema.paths is not None
+    schema = app.openapi_schema
+    paths = schema.paths
+    assert paths is not None
+    path = paths["/test"]
+    assert path.post is not None
+    parameters = path.post.parameters
+    assert parameters is not None
+    assert len(parameters) == 2
+    assert all(isinstance(param, OpenAPIParameter) for param in parameters)
+    params = cast("list[OpenAPIParameter]", parameters)
+    assert all(param.name == "name" for param in params)
+    assert tuple(param.param_in for param in params) == ("cookie", "header")