Skip to content

Commit

Permalink
Fix bugs around merging routers with nested fallbacks
Browse files Browse the repository at this point in the history
  • Loading branch information
davidpdrsn committed Jul 16, 2023
1 parent b34715f commit 1b7b872
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 11 deletions.
1 change: 1 addition & 0 deletions axum/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **added:** Add `axum::extract::Query::try_from_uri` ([#2058])
- **added:** Implement `IntoResponse` for `Box<str>` and `Box<[u8]>` ([#2035])
- **breaking:** Simplify `MethodFilter`. It no longer uses bitflags ([#2073])
- **fixed:** Fix bugs around merging routers with nested fallbacks

[#1664]: https://github.com/tokio-rs/axum/pull/1664
[#1751]: https://github.com/tokio-rs/axum/pull/1751
Expand Down
14 changes: 9 additions & 5 deletions axum/src/routing/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ impl<S> fmt::Debug for Router<S> {
pub(crate) const NEST_TAIL_PARAM: &str = "__private__axum_nest_tail_param";
pub(crate) const NEST_TAIL_PARAM_CAPTURE: &str = "/*__private__axum_nest_tail_param";
pub(crate) const FALLBACK_PARAM: &str = "__private__axum_fallback";
pub(crate) const FALLBACK_PARAM_PATH: &str = "/*__private__axum_fallback";

impl<S> Router<S>
where
Expand Down Expand Up @@ -187,7 +188,7 @@ where
{
let Router {
path_router,
fallback_router: other_fallback,
fallback_router: mut other_fallback,
default_fallback,
catch_all_fallback,
} = other.into();
Expand All @@ -198,16 +199,19 @@ where
// both have the default fallback
// use the one from other
(true, true) => {
self.fallback_router = other_fallback;
panic_on_err!(self.fallback_router.merge(other_fallback));
}
// self has default fallback, other has a custom fallback
(true, false) => {
self.fallback_router = other_fallback;
panic_on_err!(self.fallback_router.merge(other_fallback));
self.default_fallback = false;
}
// self has a custom fallback, other has a default
// nothing to do
(false, true) => {}
(false, true) => {
let fallback_router = std::mem::take(&mut self.fallback_router);
panic_on_err!(other_fallback.merge(fallback_router));
self.fallback_router = other_fallback;
}
// both have a custom fallback, not allowed
(false, false) => {
panic!("Cannot merge two `Router`s that both have a fallback")
Expand Down
28 changes: 22 additions & 6 deletions axum/src/routing/path_router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use tower_service::Service;

use super::{
future::RouteFuture, not_found::NotFound, strip_prefix::StripPrefix, url_params, Endpoint,
MethodRouter, Route, RouteId, FALLBACK_PARAM, NEST_TAIL_PARAM,
MethodRouter, Route, RouteId, FALLBACK_PARAM_PATH, NEST_TAIL_PARAM,
};

pub(super) struct PathRouter<S, const IS_FALLBACK: bool> {
Expand All @@ -28,7 +28,7 @@ where

pub(super) fn set_fallback(&mut self, endpoint: Endpoint<S>) {
self.replace_endpoint("/", endpoint.clone());
self.replace_endpoint(&format!("/*{FALLBACK_PARAM}"), endpoint);
self.replace_endpoint(FALLBACK_PARAM_PATH, endpoint);
}
}

Expand Down Expand Up @@ -136,10 +136,26 @@ where
.route_id_to_path
.get(&id)
.expect("no path for route id. This is a bug in axum. Please file an issue");
match route {
Endpoint::MethodRouter(method_router) => self.route(path, method_router)?,
Endpoint::Route(route) => self.route_service(path, route)?,
};

if IS_FALLBACK && (&**path == "/" || &**path == FALLBACK_PARAM_PATH) {
// when merging two routers it doesn't matter if you do `a.merge(b)` or
// `b.merge(a)`. This must also be true for fallbacks.
//
// However all fallback routers will have routes for `/` and `/*` so when merging
// we have to ignore the top level fallbacks on one side otherwise we get
// conflicts.
//
// `Router::merge` makes sure that when merging fallbacks `other` always has the
// fallback we want to keep. It panics if both routers have a custom fallback. Thus
// it is always okay to ignore one fallback and `Router::merge` also makes sure the
// one we can ignore is that of `self`.
self.replace_endpoint(path, route);
} else {
match route {
Endpoint::MethodRouter(method_router) => self.route(path, method_router)?,
Endpoint::Route(route) => self.route_service(path, route)?,
}
}
}

Ok(())
Expand Down
119 changes: 119 additions & 0 deletions axum/src/routing/tests/fallback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,122 @@ async fn doesnt_panic_if_used_with_nested_router() {
let res = client.get("/foobar").send().await;
assert_eq!(res.status(), StatusCode::OK);
}

#[crate::test]
async fn issue_2072() {
let nested_routes = Router::new().fallback(inner_fallback);

let app = Router::new()
.nest("/nested", nested_routes)
.merge(Router::new());

let client = TestClient::new(app);

let res = client.get("/nested/does-not-exist").send().await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
assert_eq!(res.text().await, "inner");

let res = client.get("/does-not-exist").send().await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
assert_eq!(res.text().await, "");
}

#[crate::test]
async fn issue_2072_outer_fallback_before_merge() {
let nested_routes = Router::new().fallback(inner_fallback);

let app = Router::new()
.nest("/nested", nested_routes)
.fallback(outer_fallback)
.merge(Router::new());

let client = TestClient::new(app);

let res = client.get("/nested/does-not-exist").send().await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
assert_eq!(res.text().await, "inner");

let res = client.get("/does-not-exist").send().await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
assert_eq!(res.text().await, "outer");
}

#[crate::test]
async fn issue_2072_outer_fallback_after_merge() {
let nested_routes = Router::new().fallback(inner_fallback);

let app = Router::new()
.nest("/nested", nested_routes)
.merge(Router::new())
.fallback(outer_fallback);

let client = TestClient::new(app);

let res = client.get("/nested/does-not-exist").send().await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
assert_eq!(res.text().await, "inner");

let res = client.get("/does-not-exist").send().await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
assert_eq!(res.text().await, "outer");
}

#[crate::test]
async fn merge_router_with_fallback_into_nested_router_with_fallback() {
let nested_routes = Router::new().fallback(inner_fallback);

let app = Router::new()
.nest("/nested", nested_routes)
.merge(Router::new().fallback(outer_fallback));

let client = TestClient::new(app);

let res = client.get("/nested/does-not-exist").send().await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
assert_eq!(res.text().await, "inner");

let res = client.get("/does-not-exist").send().await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
assert_eq!(res.text().await, "outer");
}

#[crate::test]
async fn merging_nested_router_with_fallback_into_router_with_fallback() {
let nested_routes = Router::new().fallback(inner_fallback);

let app = Router::new()
.fallback(outer_fallback)
.merge(Router::new().nest("/nested", nested_routes));

let client = TestClient::new(app);

let res = client.get("/nested/does-not-exist").send().await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
assert_eq!(res.text().await, "inner");

let res = client.get("/does-not-exist").send().await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
assert_eq!(res.text().await, "outer");
}

#[crate::test]
async fn merge_empty_into_router_with_fallback() {
let app = Router::new().fallback(outer_fallback).merge(Router::new());

let client = TestClient::new(app);

let res = client.get("/does-not-exist").send().await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
assert_eq!(res.text().await, "outer");
}

#[crate::test]
async fn merge_router_with_fallback_into_empty() {
let app = Router::new().merge(Router::new().fallback(outer_fallback));

let client = TestClient::new(app);

let res = client.get("/does-not-exist").send().await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
assert_eq!(res.text().await, "outer");
}

0 comments on commit 1b7b872

Please sign in to comment.