litestar-org / litestar

Production-ready, Light, Flexible and Extensible ASGI API framework | Effortlessly Build Performant APIs
https://litestar.dev/
MIT License
5.4k stars 370 forks source link

Bug: ASGI mounted at base root ("/") intercepts dynamic path params #3535

Open cemrehancavdar opened 4 months ago

cemrehancavdar commented 4 months ago

Description

ASGI mounted at base root ("/") intercepts dynamic path params. As explained by @peterschutt https://github.com/peterschutt/sqladmin-litestar-plugin/issues/11#issuecomment-2137095199

URL to code causing the issue

No response

MCVE

from litestar import Litestar, asgi, get
from litestar.types.asgi_types import Receive, Scope, Send

@get("/")
async def home() -> str:
    return "Hello, world!"

@asgi("/", is_mount=True)
async def mounted(scope: Scope, receive: Receive, send: Send) -> None:
    await send({"type": "http.response.start", "status": 200})
    await send({"type": "http.response.body", "body": b"Mounted!"})

@get("/dynamic/{name:int}")
async def dynamic(name: int) -> str:
    return f"Hello, {name}!"

@get("/not_dynamic")
async def not_dynamic() -> str:
    return "Hello, there!"

app = Litestar([home, dynamic, not_dynamic, mounted])

Steps to reproduce

1. check not_dynamic or / routes
2. It has no problem
3. /dynamic/test
4. See error

Screenshots

"![SCREENSHOT_DESCRIPTION](SCREENSHOT_LINK.png)"

Logs

No response

Litestar Version

2.8.2

Platform


[!NOTE]
While we are open for sponsoring on GitHub Sponsors and OpenCollective, we also utilize Polar.sh to engage in pledge-based sponsorship.

Check out all issues funded or available for funding on our Polar.sh dashboard

  • 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.

Fund with Polar

peterschutt commented 4 months ago

Also worth noting that if the path isn't dynamic, it is not a problem - this is b/c we handle "plain" routes separately, and before we test for mounted paths:

https://github.com/litestar-org/litestar/blob/f83c26fe834dbacbccf38dbe9c864398f05e8686/litestar/_asgi/routing_trie/traversal.py#L135-L137

We then test for mounted paths before we step through the route trie:

https://github.com/litestar-org/litestar/blob/f83c26fe834dbacbccf38dbe9c864398f05e8686/litestar/_asgi/routing_trie/traversal.py#L139-L155

This is the source of the issue, b/c the dynamic route is only resolved as part of the trie traversal, but it never gets that far because the path /whatever/123 matches the mount path /.

So, I think we'd either need to get smarter with our mount_paths_regex so that it doesn't match on routes that are registered adjacent to the mounted application with variable paths, or, traverse the routing trie before checking for a match on a mount path, and only then try to match a mount path if we'd have otherwise thrown the 404.

peterschutt commented 4 months ago

Loosely related, we also cannot mount under a variable path, b/c the mount path regex includes the path variable placeholder, i.e., re.compile("/mounted/{num:int}") doesn't match /mounted/123.

E.g.,

@asgi("/mounted/{num:int}", is_mount=True)
async def mounted(scope: Scope, receive: Receive, send: Send) -> None:
    await send({"type": "http.response.start", "status": 200})
    await send({"type": "http.response.body", "body": b"Mounted!"})
peterschutt commented 4 months ago

Maybe mounts should be routed during tree traversal, instead of separate from it. If we hit a node that has both a mount and child nodes with regular handlers, we try to route it further via the trie, and if that fails we then pipe it through the mount?

ryanolf-tb commented 3 months ago

I also came across this bug. Was going to file over at sqladmin-litestar-plugin but found it's already been raised there and pushed over here.

provinzkraut commented 3 months ago

Maybe mounts should be routed during tree traversal, instead of separate from it. If we hit a node that has both a mount and child nodes with regular handlers, we try to route it further via the trie, and if that fails we then pipe it through the mount?

Sounds solid

provinzkraut commented 3 months ago

Going back to this, I'm having second thought if we actually want to allow this. Mounting ASGI apps at the root path is always going to be kinda weird and cause unexpected behaviour in some cases, because you then have two entry points for your application. Maybe we should just not allow this in the first place and save us a lot of trouble?