-
-
Notifications
You must be signed in to change notification settings - Fork 953
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Handling trailing slashes properly with mounted routes #869
Comments
One interesting thing to point out here. As far as I understood, I could get rid of 307 by using Example: where
Actually my scenario causes me to change my desired API architecture as otherwise it won't work properly. And if I try to send request to So for now on, I use |
Similar conversation over here, and they implemented a solution: fastapi/fastapi#414 |
Hello, I've got the same question, i.e. I want to be able to request a route like "/resources", not "/resources/" when e.g. I post a resource. When I ant to rertieve a resource of id XX I'll use "/resources/XX". But It seems I can't reaaly do that. I don't really want to use FastApi. |
the same problem, mounted |
Ran into the same issue when integrating with Ariadne's ASGI.
results in
Since app is not even part of my project I cannot do anything here. In fact there is actually no way to even force a redirect, except by creating a manual route like this:
Which is a suboptimal solution as already discussed. |
From my understanding, From what I can gather, compile_path now escapes any regex tokens: >>> from starlette.routing import compile_path
>>> r_path, *_ = compile_path("/users/?")
>>> r_path
re.compile('^/users/\\?$')
>>> r_path.match("/users")
None
>>> r_path.match("/users/")
None
>>> r_path.match("/users/?")
<re.Match object; span=(0, 8), match='/users/?'> |
Running into the same thing with ariadne. We use Apollo Federation and the gateway reports my service is down every once in a while. How is this not solved yet. Did any of you guys find a solution for ariadne mounts ? |
Creating some middleware worked for me: from starlette.requests import Request
from starlette.types import ASGIApp, Receive, Scope, Send
class GraphQLRedirect:
def __init__(
self,
app: ASGIApp
) -> None:
self.app = app
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if(scope['path'] == '/graphql'):
scope['path'] = '/graphql/'
scope['raw_path'] = b'/graphql/'
await self.app(scope, receive, send) and app = FastAPI(redoc_url=None, docs_url=None, openapi_url=None)
app.add_middleware(GraphQLRedirect)
type_defs = load_schema_from_path("schema.graphql")
schema = make_executable_schema(
type_defs, query, snake_case_fallback_resolvers
)
app.mount("/graphql", GraphQL(schema)) No more 307s! |
Thanks I just added the backslash to the GQL gateway config and solved my case. But this is an issue that needs to be solved in starlette itself. I have no idea why this is not picked up yet. |
We switched from FastAPI to Starlette (since we've completely migrated to GraphQL via Ariadne), and just hit this issue as the 307 Redirect caused our web-app build to crash. Took me a while to find this page and would definitely 👍 a fix for this in Starlette |
Still happening today with the latest version. |
RE "Making the developer in complete control of trailing slashes by not emitting any implicit 307" this seems to be possible as of version 0.31.0 app = Starlette(...)
app.router.redirect_slashes = False |
It see it nowhere in the documentation. Is it stable? Should it be written somewhere? (The changelog mentions a redirect slashes" support since 0.7... not sure it's the same thing.) |
It's not documented (based on a code search). I stumbled upon it after spending several hours on this issue. It would make sense to expose this option in the Starlette constructor That, combined with @cgorski's suggestion, and I think I have a solid workaround (at least for my use case). |
For my use case (only one layer of def mount(m: Mount) -> list[Route]:
new_routes = []
# This is where I "assert" that there are no nested Mounts inside Mounts
old_routes: list[Route] = m.routes
for r in old_routes:
new_route = Route(
f"{m.path}{r.path}".rstrip("/") or "/",
r.endpoint,
methods=r.methods,
name=f"{m.name}:{r.name}",
)
new_routes.append(new_route)
return new_routes
app = Starlette(
routes=[
Route("/health", routes.health_check, name="health"),
Route("/robots.txt", routes.robots_txt, name="robots_txt"),
Route("/light_theme", routes.light_theme, name="light_theme"),
Mount(
"/static",
app=StaticFiles(
directory=pathlib.Path(__file__).parent / ".." / "static" / "build"
),
name="static",
),
]
+ mount(Mount("/auth", app=auth_router, name="auth"))
+ mount(Mount("/widgets", app=widget_router, name="widgets"))
+ mount(Mount("/foobars", app=foobar_router, name="foobars"))
+ mount(Mount("/", app=home_router, name="home")),
# etc.
) |
Checking in from 2024 and this is still an issue, and when trying to use a starlette app with Google Cloud Run it becomes even more so as cloud run does not like 307s and will sometimes just eat the response |
Hello there!
In the documentation, there is this example about large applications. If we make it a little more complete, we can get something like this:
But the problem with this example is that all routes are behaving differently:
curl localhost:8000
answers as expectedcurl localhost:8000/
answers as expectedcurl localhost:8000/users
emits a 307curl localhost:8000/users/
answers as expectedcurl localhost:8000/users/bob
answers as expectedcurl localhost:8000/users/bob/
emits a 307So one route answers as expected whether you give it a trailing slash or not, another one only answers directly if you give it the trailing slash, and another one only answers directly if you do not give it the trailing slash.
Of course, the fact that the homepage is answering with or without slash is only due to how HTTP works, because you can't send a GET without a route, and all HTTP clients will convert this to
GET /
.At this point I'm just paraphrasing #823, where the discussion ended with:
Alright, let's take that for a fact, but then... what if I want all my mounted routes to answer without a trailing slash? This is particularly important for API development, where you want to provide consistent endpoints.
I started by trying to give an empty route, i.e.
Route('', users, methods=['GET', 'POST'])
, but that simply does not work:AssertionError: Routed paths must start with '/'
.Reconsidering the answer in #823, I then tried to mount on
/
directly, i.e. doing this, thinking that it would work around my problem:That seems weird here, because you could simply add the routes at the top level, but in my real use-case, I'm using routers that I import and mount. And that works great... until you add another
Mount
, that will never be resolved, because the routing stops at the first mount point with a matching prefix, as explained in #380 (comment).Then I saw #633 (comment), and tried to add
/?
everywhere, just like this:This is not user-friendly at all, but it helped on the
/users/{username}
route, which now answers as expected whether you give it a trailing slash or not. Alas, the/users
route still emits a 307. So basically, at this point, I just don't know how to handle this, and I'm pretty frustrated by spending so much time on such a trivial thing.What I ended up doing, since I'm using routers, is defining the whole path in my routes, with a big hack at the application level:
If there isn't something obvious that I missed and would resolve my problem in a simpler way, please consider one or more of the following (by my preference order):
/?
Sorry for the long text, I couldn't find a simpler way to expose the whole problem and thinking behind it.
Important
The text was updated successfully, but these errors were encountered: