Skip to content

Commit bb406b1

Browse files
committed
Fix bugs around merging routers with nested fallbacks (#2096)
1 parent 959f4d0 commit bb406b1

File tree

4 files changed

+156
-12
lines changed

4 files changed

+156
-12
lines changed

axum/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
# Unreleased
99

10-
- None.
10+
- **fixed:** Fix bugs around merging routers with nested fallbacks ([#2096])
11+
12+
[#2096]: https://github.com/tokio-rs/axum/pull/2096
1113

1214
# 0.6.18 (30. April, 2023)
1315

axum/src/routing/mod.rs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ impl<S, B> fmt::Debug for Router<S, B> {
9898
pub(crate) const NEST_TAIL_PARAM: &str = "__private__axum_nest_tail_param";
9999
pub(crate) const NEST_TAIL_PARAM_CAPTURE: &str = "/*__private__axum_nest_tail_param";
100100
pub(crate) const FALLBACK_PARAM: &str = "__private__axum_fallback";
101+
pub(crate) const FALLBACK_PARAM_PATH: &str = "/*__private__axum_fallback";
101102

102103
impl<S, B> Router<S, B>
103104
where
@@ -185,9 +186,12 @@ where
185186
where
186187
R: Into<Router<S, B>>,
187188
{
189+
const PANIC_MSG: &str =
190+
"Failed to merge fallbacks. This is a bug in axum. Please file an issue";
191+
188192
let Router {
189193
path_router,
190-
fallback_router: other_fallback,
194+
fallback_router: mut other_fallback,
191195
default_fallback,
192196
catch_all_fallback,
193197
} = other.into();
@@ -198,16 +202,19 @@ where
198202
// both have the default fallback
199203
// use the one from other
200204
(true, true) => {
201-
self.fallback_router = other_fallback;
205+
self.fallback_router.merge(other_fallback).expect(PANIC_MSG);
202206
}
203207
// self has default fallback, other has a custom fallback
204208
(true, false) => {
205-
self.fallback_router = other_fallback;
209+
self.fallback_router.merge(other_fallback).expect(PANIC_MSG);
206210
self.default_fallback = false;
207211
}
208212
// self has a custom fallback, other has a default
209-
// nothing to do
210-
(false, true) => {}
213+
(false, true) => {
214+
let fallback_router = std::mem::take(&mut self.fallback_router);
215+
other_fallback.merge(fallback_router).expect(PANIC_MSG);
216+
self.fallback_router = other_fallback;
217+
}
211218
// both have a custom fallback, not allowed
212219
(false, false) => {
213220
panic!("Cannot merge two `Router`s that both have a fallback")

axum/src/routing/path_router.rs

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use tower_service::Service;
88

99
use super::{
1010
future::RouteFuture, not_found::NotFound, strip_prefix::StripPrefix, url_params, Endpoint,
11-
MethodRouter, Route, RouteId, FALLBACK_PARAM, NEST_TAIL_PARAM,
11+
MethodRouter, Route, RouteId, FALLBACK_PARAM_PATH, NEST_TAIL_PARAM,
1212
};
1313

1414
pub(super) struct PathRouter<S, B, const IS_FALLBACK: bool> {
@@ -30,7 +30,7 @@ where
3030

3131
pub(super) fn set_fallback(&mut self, endpoint: Endpoint<S, B>) {
3232
self.replace_endpoint("/", endpoint.clone());
33-
self.replace_endpoint(&format!("/*{FALLBACK_PARAM}"), endpoint);
33+
self.replace_endpoint(FALLBACK_PARAM_PATH, endpoint);
3434
}
3535
}
3636

@@ -139,10 +139,26 @@ where
139139
.route_id_to_path
140140
.get(&id)
141141
.expect("no path for route id. This is a bug in axum. Please file an issue");
142-
match route {
143-
Endpoint::MethodRouter(method_router) => self.route(path, method_router)?,
144-
Endpoint::Route(route) => self.route_service(path, route)?,
145-
};
142+
143+
if IS_FALLBACK && (&**path == "/" || &**path == FALLBACK_PARAM_PATH) {
144+
// when merging two routers it doesn't matter if you do `a.merge(b)` or
145+
// `b.merge(a)`. This must also be true for fallbacks.
146+
//
147+
// However all fallback routers will have routes for `/` and `/*` so when merging
148+
// we have to ignore the top level fallbacks on one side otherwise we get
149+
// conflicts.
150+
//
151+
// `Router::merge` makes sure that when merging fallbacks `other` always has the
152+
// fallback we want to keep. It panics if both routers have a custom fallback. Thus
153+
// it is always okay to ignore one fallback and `Router::merge` also makes sure the
154+
// one we can ignore is that of `self`.
155+
self.replace_endpoint(path, route);
156+
} else {
157+
match route {
158+
Endpoint::MethodRouter(method_router) => self.route(path, method_router)?,
159+
Endpoint::Route(route) => self.route_service(path, route)?,
160+
}
161+
}
146162
}
147163

148164
Ok(())

axum/src/routing/tests/fallback.rs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,3 +241,122 @@ async fn doesnt_panic_if_used_with_nested_router() {
241241
let res = client.get("/foobar").send().await;
242242
assert_eq!(res.status(), StatusCode::OK);
243243
}
244+
245+
#[crate::test]
246+
async fn issue_2072() {
247+
let nested_routes = Router::new().fallback(inner_fallback);
248+
249+
let app = Router::new()
250+
.nest("/nested", nested_routes)
251+
.merge(Router::new());
252+
253+
let client = TestClient::new(app);
254+
255+
let res = client.get("/nested/does-not-exist").send().await;
256+
assert_eq!(res.status(), StatusCode::NOT_FOUND);
257+
assert_eq!(res.text().await, "inner");
258+
259+
let res = client.get("/does-not-exist").send().await;
260+
assert_eq!(res.status(), StatusCode::NOT_FOUND);
261+
assert_eq!(res.text().await, "");
262+
}
263+
264+
#[crate::test]
265+
async fn issue_2072_outer_fallback_before_merge() {
266+
let nested_routes = Router::new().fallback(inner_fallback);
267+
268+
let app = Router::new()
269+
.nest("/nested", nested_routes)
270+
.fallback(outer_fallback)
271+
.merge(Router::new());
272+
273+
let client = TestClient::new(app);
274+
275+
let res = client.get("/nested/does-not-exist").send().await;
276+
assert_eq!(res.status(), StatusCode::NOT_FOUND);
277+
assert_eq!(res.text().await, "inner");
278+
279+
let res = client.get("/does-not-exist").send().await;
280+
assert_eq!(res.status(), StatusCode::NOT_FOUND);
281+
assert_eq!(res.text().await, "outer");
282+
}
283+
284+
#[crate::test]
285+
async fn issue_2072_outer_fallback_after_merge() {
286+
let nested_routes = Router::new().fallback(inner_fallback);
287+
288+
let app = Router::new()
289+
.nest("/nested", nested_routes)
290+
.merge(Router::new())
291+
.fallback(outer_fallback);
292+
293+
let client = TestClient::new(app);
294+
295+
let res = client.get("/nested/does-not-exist").send().await;
296+
assert_eq!(res.status(), StatusCode::NOT_FOUND);
297+
assert_eq!(res.text().await, "inner");
298+
299+
let res = client.get("/does-not-exist").send().await;
300+
assert_eq!(res.status(), StatusCode::NOT_FOUND);
301+
assert_eq!(res.text().await, "outer");
302+
}
303+
304+
#[crate::test]
305+
async fn merge_router_with_fallback_into_nested_router_with_fallback() {
306+
let nested_routes = Router::new().fallback(inner_fallback);
307+
308+
let app = Router::new()
309+
.nest("/nested", nested_routes)
310+
.merge(Router::new().fallback(outer_fallback));
311+
312+
let client = TestClient::new(app);
313+
314+
let res = client.get("/nested/does-not-exist").send().await;
315+
assert_eq!(res.status(), StatusCode::NOT_FOUND);
316+
assert_eq!(res.text().await, "inner");
317+
318+
let res = client.get("/does-not-exist").send().await;
319+
assert_eq!(res.status(), StatusCode::NOT_FOUND);
320+
assert_eq!(res.text().await, "outer");
321+
}
322+
323+
#[crate::test]
324+
async fn merging_nested_router_with_fallback_into_router_with_fallback() {
325+
let nested_routes = Router::new().fallback(inner_fallback);
326+
327+
let app = Router::new()
328+
.fallback(outer_fallback)
329+
.merge(Router::new().nest("/nested", nested_routes));
330+
331+
let client = TestClient::new(app);
332+
333+
let res = client.get("/nested/does-not-exist").send().await;
334+
assert_eq!(res.status(), StatusCode::NOT_FOUND);
335+
assert_eq!(res.text().await, "inner");
336+
337+
let res = client.get("/does-not-exist").send().await;
338+
assert_eq!(res.status(), StatusCode::NOT_FOUND);
339+
assert_eq!(res.text().await, "outer");
340+
}
341+
342+
#[crate::test]
343+
async fn merge_empty_into_router_with_fallback() {
344+
let app = Router::new().fallback(outer_fallback).merge(Router::new());
345+
346+
let client = TestClient::new(app);
347+
348+
let res = client.get("/does-not-exist").send().await;
349+
assert_eq!(res.status(), StatusCode::NOT_FOUND);
350+
assert_eq!(res.text().await, "outer");
351+
}
352+
353+
#[crate::test]
354+
async fn merge_router_with_fallback_into_empty() {
355+
let app = Router::new().merge(Router::new().fallback(outer_fallback));
356+
357+
let client = TestClient::new(app);
358+
359+
let res = client.get("/does-not-exist").send().await;
360+
assert_eq!(res.status(), StatusCode::NOT_FOUND);
361+
assert_eq!(res.text().await, "outer");
362+
}

0 commit comments

Comments
 (0)