Skip to content

Commit cd09bf3

Browse files
committed
Don't allow extracting MatchedPath in fallbacks
With the introduction of `PathRouter` in #1711 I forgot that fallbacks shouldn't be able to extract `MatchedPath`. The extension for it was interested regardless if the `PathRouter` was used as a fallback or not. It doesn't make sense to extract `MatchedPath` in a fallback because there was no matched route. Turns out it also fixes a panic with a specifical fallback/nest setup. Fixes #1931
1 parent 377d73a commit cd09bf3

5 files changed

Lines changed: 94 additions & 22 deletions

File tree

axum/CHANGELOG.md

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

88
# Unreleased
99

10-
- None.
10+
- **fixed:** Don't allow extracting `MatchedPath` in fallbacks ([#1934])
11+
- **fixed:** Fix panic if `Router` with something nested at `/` was used as a fallback ([#1934])
12+
13+
[#1934]: https://github.com/tokio-rs/axum/pull/1934
1114

1215
# 0.6.15 (12. April, 2023)
1316

axum/src/extract/matched_path.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ mod tests {
176176
Router,
177177
};
178178
use http::{Request, StatusCode};
179+
use hyper::Body;
179180

180181
#[crate::test]
181182
async fn extracting_on_handler() {
@@ -353,4 +354,19 @@ mod tests {
353354
let res = client.get("/foo/bar").send().await;
354355
assert_eq!(res.status(), StatusCode::OK);
355356
}
357+
358+
#[crate::test]
359+
async fn cant_extract_in_fallback() {
360+
async fn handler(path: Option<MatchedPath>, req: Request<Body>) {
361+
assert!(path.is_none());
362+
assert!(req.extensions().get::<MatchedPath>().is_none());
363+
}
364+
365+
let app = Router::new().fallback(handler);
366+
367+
let client = TestClient::new(app);
368+
369+
let res = client.get("/foo/bar").send().await;
370+
assert_eq!(res.status(), StatusCode::OK);
371+
}
356372
}

axum/src/routing/mod.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
//! Routing between [`Service`]s and handlers.
22
3-
use self::{future::RouteFuture, not_found::NotFound, path_router::PathRouter};
3+
use self::{
4+
future::RouteFuture,
5+
not_found::NotFound,
6+
path_router::{IsFallback, IsNotFallback, PathRouter},
7+
};
48
#[cfg(feature = "tokio")]
59
use crate::extract::connect_info::IntoMakeServiceWithConnectInfo;
610
use crate::{
@@ -57,8 +61,8 @@ pub(crate) struct RouteId(u32);
5761
/// The router type for composing handlers and services.
5862
#[must_use]
5963
pub struct Router<S = (), B = Body> {
60-
path_router: PathRouter<S, B>,
61-
fallback_router: PathRouter<S, B>,
64+
path_router: PathRouter<IsNotFallback, S, B>,
65+
fallback_router: PathRouter<IsFallback, S, B>,
6266
default_fallback: bool,
6367
}
6468

@@ -499,7 +503,7 @@ impl<S, B> fmt::Debug for Endpoint<S, B> {
499503
}
500504
}
501505

502-
struct SuperFallback<S, B>(SyncWrapper<PathRouter<S, B>>);
506+
struct SuperFallback<S, B>(SyncWrapper<PathRouter<IsFallback, S, B>>);
503507

504508
#[test]
505509
#[allow(warnings)]

axum/src/routing/path_router.rs

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
use crate::body::{Body, HttpBody};
1+
use crate::body::HttpBody;
22
use axum_core::response::IntoResponse;
33
use http::Request;
44
use matchit::MatchError;
5-
use std::{borrow::Cow, collections::HashMap, convert::Infallible, fmt, sync::Arc};
5+
use std::{
6+
borrow::Cow, collections::HashMap, convert::Infallible, fmt, marker::PhantomData, sync::Arc,
7+
};
68
use tower_layer::Layer;
79
use tower_service::Service;
810

@@ -11,14 +13,16 @@ use super::{
1113
RouteId, NEST_TAIL_PARAM,
1214
};
1315

14-
pub(super) struct PathRouter<S = (), B = Body> {
16+
pub(super) struct PathRouter<F, S, B> {
1517
routes: HashMap<RouteId, Endpoint<S, B>>,
1618
node: Arc<Node>,
1719
prev_route_id: RouteId,
20+
_is_it_a_fallback: PhantomData<F>,
1821
}
1922

20-
impl<S, B> PathRouter<S, B>
23+
impl<F, S, B> PathRouter<F, S, B>
2124
where
25+
F: IsItAFallback,
2226
B: HttpBody + Send + 'static,
2327
S: Clone + Send + Sync + 'static,
2428
{
@@ -107,11 +111,12 @@ where
107111
Ok(())
108112
}
109113

110-
pub(super) fn merge(&mut self, other: PathRouter<S, B>) -> Result<(), Cow<'static, str>> {
114+
pub(super) fn merge(&mut self, other: PathRouter<F, S, B>) -> Result<(), Cow<'static, str>> {
111115
let PathRouter {
112116
routes,
113117
node,
114118
prev_route_id: _,
119+
_is_it_a_fallback: _,
115120
} = other;
116121

117122
for (id, route) in routes {
@@ -131,14 +136,15 @@ where
131136
pub(super) fn nest(
132137
&mut self,
133138
path: &str,
134-
router: PathRouter<S, B>,
139+
router: PathRouter<F, S, B>,
135140
) -> Result<(), Cow<'static, str>> {
136141
let prefix = validate_nest_path(path);
137142

138143
let PathRouter {
139144
routes,
140145
node,
141146
prev_route_id: _,
147+
_is_it_a_fallback: _,
142148
} = router;
143149

144150
for (id, endpoint) in routes {
@@ -193,7 +199,7 @@ where
193199
Ok(())
194200
}
195201

196-
pub(super) fn layer<L, NewReqBody>(self, layer: L) -> PathRouter<S, NewReqBody>
202+
pub(super) fn layer<L, NewReqBody>(self, layer: L) -> PathRouter<F, S, NewReqBody>
197203
where
198204
L: Layer<Route<B>> + Clone + Send + 'static,
199205
L::Service: Service<Request<NewReqBody>> + Clone + Send + 'static,
@@ -215,6 +221,7 @@ where
215221
routes,
216222
node: self.node,
217223
prev_route_id: self.prev_route_id,
224+
_is_it_a_fallback: self._is_it_a_fallback,
218225
}
219226
}
220227

@@ -247,10 +254,11 @@ where
247254
routes,
248255
node: self.node,
249256
prev_route_id: self.prev_route_id,
257+
_is_it_a_fallback: self._is_it_a_fallback,
250258
}
251259
}
252260

253-
pub(super) fn with_state<S2>(self, state: S) -> PathRouter<S2, B> {
261+
pub(super) fn with_state<S2>(self, state: S) -> PathRouter<F, S2, B> {
254262
let routes = self
255263
.routes
256264
.into_iter()
@@ -269,6 +277,7 @@ where
269277
routes,
270278
node: self.node,
271279
prev_route_id: self.prev_route_id,
280+
_is_it_a_fallback: self._is_it_a_fallback,
272281
}
273282
}
274283

@@ -293,12 +302,14 @@ where
293302
Ok(match_) => {
294303
let id = *match_.value;
295304

296-
#[cfg(feature = "matched-path")]
297-
crate::extract::matched_path::set_matched_path_for_request(
298-
id,
299-
&self.node.route_id_to_path,
300-
req.extensions_mut(),
301-
);
305+
if !F::FALLBACK {
306+
#[cfg(feature = "matched-path")]
307+
crate::extract::matched_path::set_matched_path_for_request(
308+
id,
309+
&self.node.route_id_to_path,
310+
req.extensions_mut(),
311+
);
312+
}
302313

303314
url_params::insert_url_params(req.extensions_mut(), match_.params);
304315

@@ -347,17 +358,18 @@ where
347358
}
348359
}
349360

350-
impl<B, S> Default for PathRouter<S, B> {
361+
impl<F, B, S> Default for PathRouter<F, S, B> {
351362
fn default() -> Self {
352363
Self {
353364
routes: Default::default(),
354365
node: Default::default(),
355366
prev_route_id: RouteId(0),
367+
_is_it_a_fallback: PhantomData,
356368
}
357369
}
358370
}
359371

360-
impl<S, B> fmt::Debug for PathRouter<S, B> {
372+
impl<F, S, B> fmt::Debug for PathRouter<F, S, B> {
361373
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
362374
f.debug_struct("PathRouter")
363375
.field("routes", &self.routes)
@@ -366,12 +378,13 @@ impl<S, B> fmt::Debug for PathRouter<S, B> {
366378
}
367379
}
368380

369-
impl<S, B> Clone for PathRouter<S, B> {
381+
impl<F, S, B> Clone for PathRouter<F, S, B> {
370382
fn clone(&self) -> Self {
371383
Self {
372384
routes: self.routes.clone(),
373385
node: self.node.clone(),
374386
prev_route_id: self.prev_route_id,
387+
_is_it_a_fallback: self._is_it_a_fallback,
375388
}
376389
}
377390
}
@@ -443,3 +456,23 @@ pub(crate) fn path_for_nested_route<'a>(prefix: &'a str, path: &'a str) -> Cow<'
443456
format!("{prefix}{path}").into()
444457
}
445458
}
459+
460+
/// Used to statically enforce that we don't merge/nest a fallback `PathRouter` into a non-fallback
461+
/// `PathRouter`.
462+
pub(super) trait IsItAFallback {
463+
const FALLBACK: bool;
464+
}
465+
466+
#[derive(Copy, Clone)]
467+
pub(super) struct IsFallback;
468+
469+
#[derive(Copy, Clone)]
470+
pub(super) struct IsNotFallback;
471+
472+
impl IsItAFallback for IsFallback {
473+
const FALLBACK: bool = true;
474+
}
475+
476+
impl IsItAFallback for IsNotFallback {
477+
const FALLBACK: bool = false;
478+
}

axum/src/routing/tests/fallback.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,3 +225,19 @@ async fn nest_fallback_on_inner() {
225225
assert_eq!(res.status(), StatusCode::NOT_FOUND);
226226
assert_eq!(res.text().await, "inner fallback");
227227
}
228+
229+
// https://github.com/tokio-rs/axum/issues/1931
230+
#[crate::test]
231+
async fn doesnt_panic_if_used_with_nested_router() {
232+
async fn handler() {}
233+
234+
let routes_static =
235+
Router::new().nest_service("/", crate::routing::get_service(handler.into_service()));
236+
237+
let routes_all = Router::new().fallback_service(routes_static);
238+
239+
let client = TestClient::new(routes_all);
240+
241+
let res = client.get("/foobar").send().await;
242+
assert_eq!(res.status(), StatusCode::OK);
243+
}

0 commit comments

Comments
 (0)