Repository: litestar-org/litestar

Issue:
Bug: regular handler under asgi mount path conflicts with routing to asgi app
### Description

If we have a regular handler mounted under an asgi mounted path, and the path of the regular handler is a non-prefix sub-string of the a request path the request will not get routed to the asgi app.

I.e., if we have an asgi mounted on `"/"`, and a regular handler at `"/path"`, a request to `"/some/path"` does not get routed to the asgi app.

### URL to code causing the issue

_No response_

### MCVE

```python
from __future__ import annotations

from typing import TYPE_CHECKING

from litestar import Litestar, asgi, get
from litestar.testing import TestClient

if TYPE_CHECKING:
    from litestar.types.asgi_types import Receive, Scope, Send


async def asgi_app(scope: Scope, receive: Receive, send: Send) -> None:
    assert scope["type"] == "http"
    await send({
        "type": "http.response.start",
        "status": 200,
        "headers": [
            (b"content-type", b"text/plain"),
            (b"content-length", b"%d" % len(scope["raw_path"])),
        ],
    })
    await send({
        "type": "http.response.body",
        "body": scope["raw_path"],
    })

asgi_handler = asgi("/", is_mount=True)(asgi_app)


@get("/path")
def get_handler() -> str:
    return "Hello, world!"


def test_regular_handler_under_mounted_asgi_app() -> None:
    app = Litestar(
        route_handlers=[asgi("/", is_mount=True)(asgi_app), get_handler],
        openapi_config=None,
        debug=True,
    )

    with TestClient(app) as client:
        resp = client.get("/some/path")  # currently this is a 404
        assert resp.content == b"/some/path"
```


### Steps to reproduce

```bash
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
```


### Screenshots

```bash
"![SCREENSHOT_DESCRIPTION](SCREENSHOT_LINK.png)"
```


### Logs

_No response_

### Litestar Version

main

### Platform

- [X] 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/3429">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/api/github/litestar-org/litestar/issues/3429/pledge.svg?darkmode=1">
  <img alt="Fund with Polar" src="https://polar.sh/api/github/litestar-org/litestar/issues/3429/pledge.svg">
</picture>
</a>
<!-- POLAR PLEDGE BADGE END -->



Discussion:
Issue looks to come from here: https://github.com/litestar-org/litestar/blob/7814d597afadb4793e6b947a5644e36dfbab22ba/litestar/_asgi/routing_trie/traversal.py#L145-L146

Where `path == "/some/path"` and `children == ["/path"]`, the request isn't directed to the mounted application because the path contains the child path. I think instead of testing whether the sub-route is a substring of the path, we should check `path.startswith(f"{sub_route}/")`.

Resolving patch:
diff --git a/litestar/_asgi/routing_trie/traversal.py b/litestar/_asgi/routing_trie/traversal.py
--- a/litestar/_asgi/routing_trie/traversal.py
+++ b/litestar/_asgi/routing_trie/traversal.py
@@ -142,8 +142,12 @@ def parse_path_to_route(
             remaining_path = path[match.end() :]
             # since we allow regular handlers under static paths, we must validate that the request does not match
             # any such handler.
-            children = [sub_route for sub_route in mount_node.children or [] if sub_route != mount_path]
-            if not children or all(sub_route not in path for sub_route in children):  # type: ignore[operator]
+            children = (
+                normalize_path(sub_route)
+                for sub_route in mount_node.children or []
+                if sub_route != mount_path and isinstance(sub_route, str)
+            )
+            if not any(remaining_path.startswith(f"{sub_route}/") for sub_route in children):
                 asgi_app, handler = parse_node_handlers(node=mount_node, method=method)
                 remaining_path = remaining_path or "/"
                 if not mount_node.is_static:


Test patch:
diff --git a/tests/e2e/test_regular_handler_under_asgi_mount_path.py b/tests/e2e/test_regular_handler_under_asgi_mount_path.py
new file mode 100644
--- /dev/null
+++ b/tests/e2e/test_regular_handler_under_asgi_mount_path.py
@@ -0,0 +1,53 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from litestar import Litestar, asgi, get
+from litestar.enums import ScopeType
+from litestar.testing import TestClient
+
+if TYPE_CHECKING:
+    from litestar.types.asgi_types import Receive, Scope, Send
+
+
+async def asgi_app(scope: Scope, receive: Receive, send: Send) -> None:
+    assert scope["type"] == ScopeType.HTTP
+    await send(
+        {
+            "type": "http.response.start",
+            "status": 200,
+            "headers": [
+                (b"content-type", b"text/plain"),
+                (b"content-length", b"%d" % len(scope["raw_path"])),
+            ],
+        }
+    )
+    await send(
+        {
+            "type": "http.response.body",
+            "body": scope["raw_path"],
+            "more_body": False,
+        }
+    )
+
+
+asgi_handler = asgi("/", is_mount=True)(asgi_app)
+
+
+@get("/path")
+async def get_handler() -> str:
+    return "Hello, world!"
+
+
+app = Litestar(
+    route_handlers=[asgi_handler, get_handler],
+    openapi_config=None,
+    debug=True,
+)
+
+
+def test_regular_handler_under_mounted_asgi_app() -> None:
+    # https://github.com/litestar-org/litestar/issues/3429
+    with TestClient(app) as client:
+        resp = client.get("/some/path")
+        assert resp.content == b"/some/path"