Open gnieto opened 2 years ago
I've looked into this a bit and don't think its easy to solve unfortunately.
.route_with_tsr("/a", get(handler))
does two things:
/a
that calls your handler/a/
that accepts any HTTP method and redirects to /a
So when you do an additional .route_with_tsr("/a", post(handler))
that will conflict on 2. because it tries to add a catch-all route for /a/
which already exists so it panics.
axum-extra
cannot inspect the actual route before adding it, because its a separate crate and that data is private to the Router
, and we don't wanna expose that in general.
I can see two options:
Router::try_route
and Router::try_route_service
which wont panic on overlapping routes but instead return a Result
. Then RouterExt::route_with_tsr
can just ignore any errors it might get.I have considered adding panic-free router
methods before but require some thinking around what the error type should be and how much we want to expose there. Doing that is a backwards compatible change so we should just document this limitation until we have a proper fix.
How about adding a method to MethodRouter
that gets you a MethodFilter
of which methods the router has handlers / services for? Then we could redirect only those. That would mean no redirect if it's going to end up with a method-not-allowed error anyways, which seems like an advantage to me.
I still think that would require getting the MethodRouter
at a given route, from Router
. So before adding /a/
we could check if there already is something at that route and then inspect which methods it has handlers for.
No, we can call that method on the MethodRouter
given to route_with_tsr
.
I think this can be solved by relying on matchit's tsr errors like axum used to, but only for routes that want that behavior. So the handler trait would get a method should_tsr
(default returns false) that if true, means axum checks for a matchit tsr error on failure and redirects.
Would it be possible to pass {any, get, post, .. }
as a parameter? Then route_with_tsr
could wrap the alternative handler with it. Here's how I did it:
#[inline]
fn tsr_redirect_route(path: &'_ str) -> (Cow<'_, str>, fn(Uri) -> Response) {
fn redirect_handler(uri: Uri) -> Response {
let new_uri = map_path(uri, |path| {
path.strip_suffix('/')
.map(Cow::Borrowed)
.unwrap_or_else(|| Cow::Owned(format!("{path}/")))
});
if let Some(new_uri) = new_uri {
Redirect::permanent(&new_uri.to_string()).into_response()
} else {
StatusCode::BAD_REQUEST.into_response()
}
}
if let Some(path_without_trailing_slash) = path.strip_suffix('/') {
(Cow::Borrowed(path_without_trailing_slash), redirect_handler)
} else {
(Cow::Owned(format!("{path}/")), redirect_handler)
}
}
#[inline]
async fn tsr_handler_into_async(u: Uri, h: fn(Uri) -> Response) -> Response {
h(u)
}
and then in a Router
method
fn typed_get<H, T, P>(mut self, handler: H) -> Self
where
H: axum::handler::Handler<T, S, B>,
T: SecondElementIs<P> + 'static,
P: TypedPath,
{
let (tsr_path, tsr_handler) = tsr_redirect_route(P::PATH);
self = self.route(
tsr_path.as_ref(),
axum::routing::get(move |url| tsr_handler_into_async(url, tsr_handler)),
);
self = self.route(P::PATH, axum::routing::get(handler));
self
}
I couldn't quickly find if I could make tsr_redirect_route
generic over any, get, post, ..
so I gave it up because I already knew the methods I would use, since it's for typed paths. Could this be done with generics?
How is that related to this bug?
But to answer your question you probably want MethodRouter::or
. For example .route("/", on(MethodFilter::GET | MethodFilter::POST, handler))
Because route_with_tsr uses any, so doing
.route_with_tsr("/test", get(handler)) // [1]
.route_with_tsr("/test", post(handler))
Doesn't work because [1] sets an any handler for the alternative route. I got the same problem as the OP. Did I misunderstand the cause?
You can do .route_with_tsr("/test", get(handler).post(handler))
Bug Report
Version
v0.6.0-rc.1
Crates
axum v0.6.0-rc1 axum-extra v0.4.0-rc.1
Description
Release notes for the RC 0.6.0 mentions that trailing slashes support was removed from axum and suggests using
route_with_tsr
fromaxum-extra
. My understanding from the release notes is that the suggested function should have the same behaviour than usingroute
in version 0.5.For example, the following code in v0.5 works and redirects the trailing slashes as expected:
While trying v0.6.0-rc1, I tried replacing
route
byroute_with_trs
in order to maintain the behaviour, but the applications panics once is ran:When I run the application, I receive this panic:
If the route is injected in a single function call like this, the application does not panic:
After looking at the code,
route_with_tsr
is unconditionally registering a service with the redirect request with the "trailing slash" unconditionally, which provokes the mentioned panic. The second version, only callsroute_with_tsr
once for this route, so the service is registered once and the panic is not triggered.