Skip to content

Commit

Permalink
Fix bugs around merging routers with nested fallbacks (#2096)
Browse files Browse the repository at this point in the history
  • Loading branch information
davidpdrsn committed Jul 16, 2023
1 parent 959f4d0 commit bb406b1
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 12 deletions.
4 changes: 3 additions & 1 deletion axum/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

# Unreleased

- None.
- **fixed:** Fix bugs around merging routers with nested fallbacks ([#2096])

[#2096]: https://github.com/tokio-rs/axum/pull/2096

# 0.6.18 (30. April, 2023)

Expand Down
17 changes: 12 additions & 5 deletions axum/src/routing/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ impl<S, B> fmt::Debug for Router<S, B> {
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, B> Router<S, B>
where
Expand Down Expand Up @@ -185,9 +186,12 @@ where
where
R: Into<Router<S, B>>,
{
const PANIC_MSG: &str =
"Failed to merge fallbacks. This is a bug in axum. Please file an issue";

let Router {
path_router,
fallback_router: other_fallback,
fallback_router: mut other_fallback,
default_fallback,
catch_all_fallback,
} = other.into();
Expand All @@ -198,16 +202,19 @@ where
// both have the default fallback
// use the one from other
(true, true) => {
self.fallback_router = other_fallback;
self.fallback_router.merge(other_fallback).expect(PANIC_MSG);
}
// self has default fallback, other has a custom fallback
(true, false) => {
self.fallback_router = other_fallback;
self.fallback_router.merge(other_fallback).expect(PANIC_MSG);
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);
other_fallback.merge(fallback_router).expect(PANIC_MSG);
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 @@ -8,7 +8,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, B, const IS_FALLBACK: bool> {
Expand All @@ -30,7 +30,7 @@ where

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

Expand Down Expand Up @@ -139,10 +139,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 @@ -241,3 +241,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 bb406b1

Please sign in to comment.