starlette
starlette copied to clipboard
How to use request.url_for in mounted apps
I'm trying to compose apps from smaller Starlette apps. I can't find a way to make mounted apps that work regardless of app that is mounting them when using url_for.
In code below the mounted app can't use its internal names. It has to be somehow made aware that it's now running under namespace set by root app. Otherwise route cannot be found. With correct name hierarchy reversing URL does work.
from starlette.applications import Starlette
from starlette.responses import PlainTextResponse
from starlette.routing import Route, Mount
async def sub(request):
return PlainTextResponse(f"Subpage {request.url_for('sub')}") # doesn't work
# return PlainTextResponse(f"Subpage {request.url_for('under:sub')}") # works
subroutes = [
Route("/", endpoint=sub, name="sub")
]
subapp = Starlette(routes=subroutes)
async def homepage(request):
return PlainTextResponse("Homepage")
routes = [
Mount("/under", subapp, name="under"),
Route("/", endpoint=homepage, name="root")
]
app = Starlette(routes=routes)
Are you trying to get /under/
(the canonical path, possibly with the app's root_path prepended to it) or /
(the app-local path) here?
I remember trying something similar under Starlette 12.12 when reporting tiangolo/fastapi#829 and it works mostly fine if I convert it to vanilla Starlette using the old syntax.
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route, Mount, Request
app = Starlette()
@app.route("/app", ['GET'])
def read_main():
return JSONResponse({"message": "Hello World from main app"})
subapp = Starlette()
@subapp.route("/sub", ['GET'])
def read_sub(request: Request):
return JSONResponse({
#"root_path": request.scope['root_path'],
#"raw_path": request.scope['raw_path'],
#"path": request.scope['path'],
"req_url_for": request.url_for("read_sub"),
"app_url_for": app.url_path_for("read_sub"),
"subapp_url_for": subapp.url_path_for("read_sub"),
})
app.mount("/subapp", subapp)
{
"req_url_for":"http://127.0.0.1:8000/subapp/sub",
"app_url_for":"/subapp/sub",
"subapp_url_for":"/sub"
}
req_url_for
would become the absolute URL (with an authority component, though, which is not ideal, but that's another issue) for that route, so could that be a regression due to the routing changes with Starlette 0.13?
I am trying to get app's URL path wherever it is under ASGI hierarchy. I except app can be mounted anywhere and still keep using its internal route names. To my mind there is no app local URLs. Only URLs that are in effect for given app in its given ASGI context. In same vein a sub app should be able to produce full URLs (as with request.url_for
) that contain protocol, hostname and port by itself, without knowledge of root app nor path name hierarchy.
To get working URLs for apps under ASGI hierarchy I need to somehow give them their enclosing route name hierarchy (like under
in this example). Without it app cannot build valid URLs.
I tried the now deprecated route decorators per your example. Subapp's local URL is incorrect (for my use case) with this approach also. I also checked earlier Starlette releases, 0.10 and 0.9. Sub app's URLs didn't work there either. I get URLs that are valid only when app is the root in ASGI. Here both request.url_for
and subapp.url_path_for
should recognize sub app's internal route name sub
and yet produce URLs containing http://localhost:8000/under/
and /under/
respectively.
from starlette.applications import Starlette
from starlette.responses import PlainTextResponse
from starlette.routing import Route, Mount
app = Starlette()
subapp = Starlette()
@subapp.route("/", ['GET'], name="sub")
async def sub(request):
url = subapp.url_path_for("sub")
return PlainTextResponse(f"Subpage {request.url_for('under:sub')} vs {url}")
@app.route("/", ['GET'], name="root")
async def homepage(request):
return PlainTextResponse(f"Homepage")
app.mount("/under", subapp, name="under")
@jussiarpalahti I'm not sure what the drawback is for instantiating Starlette()
twice.
You could just do the following:
from starlette.applications import Starlette
from starlette.responses import PlainTextResponse
from starlette.routing import Route, Mount
app = Starlette()
subapp = Router()
app.mount("/under", subapp, name="under")
@subapp.route("/", ['GET'], name="sub")
async def sub(request):
return PlainTextResponse(f"Subpage root")
@subapp.route("/two", ['GET'], name="subtwo")
async def subtwo(request):
return PlainTextResponse(f"Subpage two is here")
@app.route("/", ['GET'], name="root")
async def homepage(request):
return PlainTextResponse(f"Homepage")
using multiple Starlette
instances seems like an anti-pattern.
I've contributed a fix in #1416 that tries first to look up routes in the local app, and then fails back to the global router.
So with this fix both
return PlainTextResponse(f"Subpage {request.url_for('sub')}") # didn't work, fix in #1416
return PlainTextResponse(f"Subpage {request.url_for('under:sub')}") # still works
will return the correct url.
I don't see why this comment doesn't satisfy the needs here. Can someone explain?
I don't see much engagement here, so I'll close the issue as stale, and if someone has a strong case, I'll reopen it. 🙏