Fix bugs around merging routers with nested fallbacks (#2096)

This commit is contained in:
David Pedersen 2023-07-16 12:06:58 +02:00 committed by GitHub
parent 6f7ff85565
commit a0db77a900
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 155 additions and 11 deletions

View file

@ -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 ([#2096])
[#1664]: https://github.com/tokio-rs/axum/pull/1664
[#1751]: https://github.com/tokio-rs/axum/pull/1751
@ -68,6 +69,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#1972]: https://github.com/tokio-rs/axum/pull/1972
[#2058]: https://github.com/tokio-rs/axum/pull/2058
[#2073]: https://github.com/tokio-rs/axum/pull/2073
[#2096]: https://github.com/tokio-rs/axum/pull/2096
# 0.6.17 (25. April, 2023)

View file

@ -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
@ -185,9 +186,12 @@ where
where
R: Into<Router<S>>,
{
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();
@ -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")

View file

@ -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> {
@ -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);
}
}
@ -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(())

View file

@ -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");
}