Repository: litestar-org/litestar

Issue:
Bug: Caching route handlers with the same path but different methods leads to overwriting of cache
### Description

Caching route handlers with the same path (`/` in this example) but different methods (`GET` and `POST` in this example) leads to overwriting of cache. The comments above the assert statements illustrate the current vs expected behavior.

This is related to https://github.com/litestar-org/litestar/issues/2573 and is not restricted to just `OPTIONS` and `GET`

### URL to code causing the issue

_No response_

### MCVE

```python
from litestar import get, post
from litestar.testing import create_test_client


@get(cache=10)
async def something() -> str:
    return "text"


@post(cache=10)
async def something_post() -> str:
    return "text_post"


with create_test_client([something, something_post]) as client:
    response = client.post("")
    assert response.status_code == 201
    assert response.text == "text_post"

    # these shouldn't pass, but they do
    response = client.get("")
    assert response.status_code == 201
    assert response.text == "text_post"

    # these should pass, but they don't
    response = client.get("")
    assert response.status_code == 200
    assert response.text == "text"
```


### Steps to reproduce

```bash
1. Run the code
2. The second set of asserts should fail, the third set of asserts should pass
```


### Screenshots

```bash
"![SCREENSHOT_DESCRIPTION](SCREENSHOT_LINK.png)"
```


### Logs

_No response_

### Litestar Version

GitHub - main (fd06486e2ad4ed0a41636659fec4f093a09e3dd0) as of creating this issue

### Platform

- [ ] Linux
- [X] 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 Litestar 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/2588">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/api/github/litestar-org/litestar/issues/2588/pledge.svg?darkmode=1">
  <img alt="Fund with Polar" src="https://polar.sh/api/github/litestar-org/litestar/issues/2588/pledge.svg">
</picture>
</a>
<!-- POLAR PLEDGE BADGE END -->



Discussion:

Resolving patch:
diff --git a/litestar/config/response_cache.py b/litestar/config/response_cache.py
--- a/litestar/config/response_cache.py
+++ b/litestar/config/response_cache.py
@@ -28,7 +28,8 @@ class CACHE_FOREVER:  # noqa: N801
 
 
 def default_cache_key_builder(request: Request[Any, Any, Any]) -> str:
-    """Given a request object, returns a cache key by combining the path with the sorted query params.
+    """Given a request object, returns a cache key by combining
+    the request method and path with the sorted query params.
 
     Args:
         request: request used to generate cache key.
@@ -38,7 +39,7 @@ def default_cache_key_builder(request: Request[Any, Any, Any]) -> str:
     """
     query_params: list[tuple[str, Any]] = list(request.query_params.dict().items())
     query_params.sort(key=lambda x: x[0])
-    return request.url.path + urlencode(query_params, doseq=True)
+    return request.method + request.url.path + urlencode(query_params, doseq=True)
 
 
 def default_do_cache_predicate(_: HTTPScope, status_code: int) -> bool:


Test patch:
diff --git a/tests/e2e/test_response_caching.py b/tests/e2e/test_response_caching.py
--- a/tests/e2e/test_response_caching.py
+++ b/tests/e2e/test_response_caching.py
@@ -8,12 +8,12 @@
 import msgspec
 import pytest
 
-from litestar import Litestar, Request, Response, get
+from litestar import Litestar, Request, Response, get, post
 from litestar.config.compression import CompressionConfig
 from litestar.config.response_cache import CACHE_FOREVER, ResponseCacheConfig
 from litestar.enums import CompressionEncoding
 from litestar.middleware.response_cache import ResponseCacheMiddleware
-from litestar.status_codes import HTTP_200_OK, HTTP_400_BAD_REQUEST, HTTP_500_INTERNAL_SERVER_ERROR
+from litestar.status_codes import HTTP_200_OK, HTTP_201_CREATED, HTTP_400_BAD_REQUEST, HTTP_500_INTERNAL_SERVER_ERROR
 from litestar.stores.base import Store
 from litestar.stores.memory import MemoryStore
 from litestar.testing import TestClient, create_test_client
@@ -111,9 +111,9 @@ def handler() -> None:
         client.get("/cached")
 
     if expected_expiration is None:
-        assert memory_store._store["/cached"].expires_at is None
+        assert memory_store._store["GET/cached"].expires_at is None
     else:
-        assert memory_store._store["/cached"].expires_at
+        assert memory_store._store["GET/cached"].expires_at
 
 
 def test_cache_forever(memory_store: MemoryStore) -> None:
@@ -126,7 +126,7 @@ async def handler() -> None:
     with TestClient(app) as client:
         client.get("/cached")
 
-    assert memory_store._store["/cached"].expires_at is None
+    assert memory_store._store["GET/cached"].expires_at is None
 
 
 @pytest.mark.parametrize("sync_to_thread", (True, False))
@@ -164,7 +164,7 @@ def handler() -> str:
 
         assert mock.call_count == 1
 
-    assert await app.stores.get("some_store").exists("/")
+    assert await app.stores.get("some_store").exists("GET/")
 
 
 async def test_with_stores(store: Store, mock: MagicMock) -> None:
@@ -243,7 +243,7 @@ def handler_fn() -> str:
     with TestClient(app) as client:
         client.get("/", headers={"Accept-Encoding": str(CompressionEncoding.GZIP.value)})
 
-    stored_value = await app.response_cache_config.get_store_from_app(app).get("/")
+    stored_value = await app.response_cache_config.get_store_from_app(app).get("GET/")
     assert stored_value
     stored_messages = msgspec.msgpack.decode(stored_value)
     assert gzip.decompress(stored_messages[1]["body"]).decode() == return_value
@@ -286,3 +286,36 @@ def handler() -> str:
         client.get("/")
         client.get("/")
         assert mock.call_count == 2
+
+
+def test_on_multiple_handlers(mock: MagicMock) -> None:
+    @get("/cached-local", cache=10)
+    async def handler() -> str:
+        mock()
+        return "get_response"
+
+    @post("/cached-local", cache=10)
+    async def handler_post() -> str:
+        mock()
+        return "post_response"
+
+    with create_test_client([handler, handler_post], after_request=after_request_handler) as client:
+        # POST request to have this cached
+        first_post_response = client.post("/cached-local")
+        assert first_post_response.status_code == HTTP_201_CREATED
+        assert first_post_response.text == "post_response"
+        assert mock.call_count == 1
+
+        # GET request to verify it doesn't use the cache created by the previous POST request
+        get_response = client.get("/cached-local")
+        assert get_response.status_code == HTTP_200_OK
+        assert get_response.text == "get_response"
+        assert first_post_response.headers["unique-identifier"] != get_response.headers["unique-identifier"]
+        assert mock.call_count == 2
+
+        # POST request to verify it uses the cache generated during the initial POST request
+        second_post_response = client.post("/cached-local")
+        assert second_post_response.status_code == HTTP_201_CREATED
+        assert second_post_response.text == "post_response"
+        assert first_post_response.headers["unique-identifier"] == second_post_response.headers["unique-identifier"]
+        assert mock.call_count == 2