Fallback to calling next route if no methods match (#224)

This removes a small foot gun from the routing.

This means matching different HTTP methods for the same route that
aren't defined together now works.

So `Router::new().route("/", get(...)).route("/", post(...))` now
accepts both `GET` and `POST`. Previously only `POST` would be accepted.
This commit is contained in:
David Pedersen
2021-08-21 01:00:12 +02:00
committed by GitHub
parent 971c0a394a
commit 0d8f8b7b6c
6 changed files with 112 additions and 94 deletions
+22 -39
View File
@@ -597,6 +597,7 @@ impl<S, F, B> Service<Request<B>> for Route<S, F>
where
S: Service<Request<B>, Response = Response<BoxBody>> + Clone,
F: Service<Request<B>, Response = Response<BoxBody>, Error = S::Error> + Clone,
B: Send + Sync + 'static,
{
type Response = Response<BoxBody>;
type Error = S::Error;
@@ -610,7 +611,7 @@ where
if let Some(captures) = self.pattern.full_match(&req) {
insert_url_params(&mut req, captures);
let fut = self.svc.clone().oneshot(req);
RouteFuture::a(fut)
RouteFuture::a(fut, self.fallback.clone())
} else {
let fut = self.fallback.clone().oneshot(req);
RouteFuture::b(fut)
@@ -689,12 +690,23 @@ where
Poll::Ready(Ok(()))
}
fn call(&mut self, request: Request<B>) -> Self::Future {
fn call(&mut self, mut request: Request<B>) -> Self::Future {
if self.status == StatusCode::METHOD_NOT_ALLOWED {
// we're inside a route but there was no method that matched
// so record that so we can override the status if no other
// routes match
request.extensions_mut().insert(NoMethodMatch);
}
if self.status == StatusCode::NOT_FOUND
&& request.extensions().get::<NoMethodMatch>().is_some()
{
self.status = StatusCode::METHOD_NOT_ALLOWED;
}
let mut res = Response::new(crate::body::empty());
if request.extensions().get::<OrDepth>().is_some() {
res.extensions_mut().insert(FromEmptyRouter { request });
}
res.extensions_mut().insert(FromEmptyRouter { request });
*res.status_mut() = self.status;
EmptyRouterFuture {
@@ -703,6 +715,9 @@ where
}
}
#[derive(Clone, Copy)]
struct NoMethodMatch;
/// Response extension used by [`EmptyRouter`] to send the request back to [`Or`] so
/// the other service can be called.
///
@@ -713,39 +728,6 @@ struct FromEmptyRouter<B> {
request: Request<B>,
}
/// We need to track whether we're inside an `Or` or not, and only if we then
/// should we save the request into the response extensions.
///
/// This is to work around https://github.com/hyperium/hyper/issues/2621.
///
/// Since ours can be nested we have to track the depth to know when we're
/// leaving the top most `Or`.
///
/// Hopefully when https://github.com/hyperium/hyper/issues/2621 is resolved we
/// can remove this nasty hack.
#[derive(Debug)]
struct OrDepth(usize);
impl OrDepth {
fn new() -> Self {
Self(1)
}
fn increment(&mut self) {
self.0 += 1;
}
fn decrement(&mut self) {
self.0 -= 1;
}
}
impl PartialEq<usize> for &mut OrDepth {
fn eq(&self, other: &usize) -> bool {
self.0 == *other
}
}
#[derive(Debug, Clone)]
pub(crate) struct PathPattern(Arc<Inner>);
@@ -945,6 +927,7 @@ impl<S, F, B> Service<Request<B>> for Nested<S, F>
where
S: Service<Request<B>, Response = Response<BoxBody>> + Clone,
F: Service<Request<B>, Response = Response<BoxBody>, Error = S::Error> + Clone,
B: Send + Sync + 'static,
{
type Response = Response<BoxBody>;
type Error = S::Error;
@@ -966,7 +949,7 @@ where
insert_url_params(&mut req, captures);
let fut = self.svc.clone().oneshot(req);
RouteFuture::a(fut)
RouteFuture::a(fut, self.fallback.clone())
} else {
let fut = self.fallback.clone().oneshot(req);
RouteFuture::b(fut)