Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update matchit and fix nesting inconsistencies #1086

Merged
merged 23 commits into from Aug 11, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2ddd0a2
Break `Router::nest` into `nest` and `nest_service` methods
davidpdrsn Jun 11, 2022
23721f2
Merge branch 'main' into separate-nesting-opaque-services
davidpdrsn Jun 29, 2022
2c54bb5
fix doc tests
davidpdrsn Jun 29, 2022
014067e
update docs
davidpdrsn Jun 29, 2022
79daaf0
fix
davidpdrsn Jun 29, 2022
276a950
Only accept `Router` in `Resource::{nest, nest_collection}`
davidpdrsn Jun 29, 2022
b220778
update changelog
davidpdrsn Jun 29, 2022
fb5219f
fix docs
davidpdrsn Jun 29, 2022
657a089
fix `MatchedPath` with `Router`s nested with `nest_service`
davidpdrsn Jun 29, 2022
bbf1852
Apply suggestions from code review
davidpdrsn Jun 29, 2022
7ed35d2
Merge branch 'main' into separate-nesting-opaque-services
davidpdrsn Jun 29, 2022
ca95dd6
Merge branch 'main' into separate-nesting-opaque-services
davidpdrsn Jul 3, 2022
581b0d9
adjust docs for fallbacks
davidpdrsn Jul 4, 2022
f18df8a
Always nest services as opaque
davidpdrsn Jul 26, 2022
a8b0608
Merge branch 'main' into separate-nesting-opaque-services
davidpdrsn Jul 26, 2022
b166548
fix old docs reference
davidpdrsn Jul 26, 2022
4630476
more tests for `MatchedPath` with nested handlers
davidpdrsn Jul 26, 2022
f6db084
Merge branch 'main' into separate-nesting-opaque-services
davidpdrsn Jul 26, 2022
32e67aa
minor clean up
davidpdrsn Aug 11, 2022
4c39785
Merge branch 'main' into separate-nesting-opaque-services
davidpdrsn Aug 11, 2022
be864ea
use identifier captures in format strings
davidpdrsn Aug 11, 2022
171171d
Apply suggestions from code review
davidpdrsn Aug 11, 2022
35c616d
fix test
davidpdrsn Aug 11, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions axum-extra/src/routing/resource.rs
Expand Up @@ -146,7 +146,7 @@ where
T::Future: Send + 'static,
{
let path = self.show_update_destroy_path();
self.router = self.router.nest(&path, svc);
self.router = self.router.nest_service(&path, svc);
self
}

Expand All @@ -159,7 +159,7 @@ where
T::Future: Send + 'static,
{
let path = self.index_create_path();
self.router = self.router.nest(&path, svc);
self.router = self.router.nest_service(&path, svc);
self
}

Expand Down
2 changes: 1 addition & 1 deletion axum-extra/src/routing/spa.rs
Expand Up @@ -161,7 +161,7 @@ where
.handle_error(spa.handle_error.clone());

Router::new()
.nest(&spa.paths.assets_path, assets_service)
.nest_service(&spa.paths.assets_path, assets_service)
.fallback(
get_service(ServeFile::new(&spa.paths.index_file)).handle_error(spa.handle_error),
)
Expand Down
11 changes: 11 additions & 0 deletions axum/CHANGELOG.md
Expand Up @@ -9,6 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- **added:** Support resolving host name via `Forwarded` header in `Host`
extractor ([#1078])
- **breaking:** `Router::nest` now only accepts `Router`s. Use
davidpdrsn marked this conversation as resolved.
Show resolved Hide resolved
`Router::nest_service` to nest opaque services
- **added:** Add `Router::nest_service` for nesting opaque services. Use this to
nest services like `tower::services::ServeDir`
- **breaking:** The route `/foo/` no longer matches `/foo/*rest`. If you want
to match `/foo/` you have to add a route specifically for that
- **breaking:** Path params for wildcard routes no longer include the prefix
`/`. e.g. `/foo.js` will match `/*filepath` with a value of `foo.js`, _not_
`/foo.js`
- **fixed:** Routes like `/foo` and `/*rest` are no longer considered
overlapping. `/foo` will take priority

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

Expand Down
4 changes: 3 additions & 1 deletion axum/Cargo.toml
Expand Up @@ -33,7 +33,9 @@ http = "0.2.5"
http-body = "0.4.4"
hyper = { version = "0.14.14", features = ["server", "tcp", "stream"] }
itoa = "1.0.1"
matchit = "0.5.0"
# TODO(david): cannot ship until matchit has released a new version but we can
# start making changes
matchit = { git = "https://github.com/ibraheemdev/matchit", branch = "catchall-revamp" }
memchr = "2.4.1"
mime = "0.3.16"
percent-encoding = "2.1"
Expand Down
6 changes: 4 additions & 2 deletions axum/src/extract/matched_path.rs
Expand Up @@ -149,12 +149,14 @@ mod tests {
"/public",
Router::new().route("/assets/*path", get(handler)),
)
.nest("/foo", handler.into_service())
.nest_service("/foo", handler.into_service())
.layer(tower::layer::layer_fn(SetMatchedPathExtension));

let client = TestClient::new(app);

let res = client.get("/foo").send().await;
// we cannot call `/foo` because `nest_service("/foo", _)` registers routes
davidpdrsn marked this conversation as resolved.
Show resolved Hide resolved
// for `/foo/*rest` and `/foo`
let res = client.get("/public").send().await;
assert_eq!(res.text().await, "/:key");

let res = client.get("/api/users/123").send().await;
Expand Down
4 changes: 2 additions & 2 deletions axum/src/extract/path/mod.rs
Expand Up @@ -499,10 +499,10 @@ mod tests {
let client = TestClient::new(app);

let res = client.get("/foo/bar/baz").send().await;
assert_eq!(res.text().await, "/bar/baz");
assert_eq!(res.text().await, "bar/baz");

let res = client.get("/bar/baz/qux").send().await;
assert_eq!(res.text().await, "/baz/qux");
assert_eq!(res.text().await, "baz/qux");
}

#[tokio::test]
Expand Down
165 changes: 67 additions & 98 deletions axum/src/routing/mod.rs
Expand Up @@ -3,7 +3,7 @@
use self::{future::RouteFuture, not_found::NotFound};
use crate::{
body::{boxed, Body, Bytes, HttpBody},
extract::connect_info::IntoMakeServiceWithConnectInfo,
extract::{connect_info::IntoMakeServiceWithConnectInfo, MatchedPath},
response::{IntoResponse, Redirect, Response},
routing::strip_prefix::StripPrefix,
util::try_downcast,
Expand Down Expand Up @@ -66,7 +66,6 @@ pub struct Router<B = Body> {
routes: HashMap<RouteId, Endpoint<B>>,
node: Node,
fallback: Fallback<B>,
nested_at_root: bool,
}

impl<B> Clone for Router<B> {
Expand All @@ -75,7 +74,6 @@ impl<B> Clone for Router<B> {
routes: self.routes.clone(),
node: self.node.clone(),
fallback: self.fallback.clone(),
nested_at_root: self.nested_at_root,
}
}
}
Expand All @@ -95,13 +93,11 @@ impl<B> fmt::Debug for Router<B> {
.field("routes", &self.routes)
.field("node", &self.node)
.field("fallback", &self.fallback)
.field("nested_at_root", &self.nested_at_root)
.finish()
}
}

pub(crate) const NEST_TAIL_PARAM: &str = "__private__axum_nest_tail_param";
const NEST_TAIL_PARAM_CAPTURE: &str = "/*__private__axum_nest_tail_param";

impl<B> Router<B>
where
Expand All @@ -116,7 +112,6 @@ where
routes: Default::default(),
node: Default::default(),
fallback: Fallback::Default(Route::new(NotFound)),
nested_at_root: false,
}
}

Expand Down Expand Up @@ -163,20 +158,17 @@ where
};

if let Err(err) = self.node.insert(path, id) {
self.panic_on_matchit_error(err);
panic!("Invalid route: {}", err);
}

self.routes.insert(id, service);

self
}

// TODO(david): update docs
#[doc = include_str!("../docs/routing/nest.md")]
pub fn nest<T>(mut self, mut path: &str, svc: T) -> Self
where
T: Service<Request<B>, Response = Response, Error = Infallible> + Clone + Send + 'static,
T::Future: Send + 'static,
{
pub fn nest(mut self, mut path: &str, router: Router<B>) -> Self {
if path.is_empty() {
// nesting at `""` and `"/"` should mean the same thing
path = "/";
Expand All @@ -188,65 +180,74 @@ where

let prefix = path;

if path == "/" {
self.nested_at_root = true;
let Router {
mut routes,
node,
fallback,
} = router;

if let Fallback::Custom(_) = fallback {
panic!("Cannot nest `Router`s that has a fallback");
}

match try_downcast::<Router<B>, _>(svc) {
// if the user is nesting a `Router` we can implement nesting
// by simplying copying all the routes and adding the prefix in
// front
Ok(router) => {
let Router {
mut routes,
node,
fallback,
// nesting a router that has something nested at root
// doesn't mean something is nested at root in _this_ router
// thus we don't need to propagate that
nested_at_root: _,
} = router;

if let Fallback::Custom(_) = fallback {
panic!("Cannot nest `Router`s that has a fallback");
}
for (id, nested_path) in node.route_id_to_path {
let route = routes.remove(&id).unwrap();
let full_path: Cow<str> = if &*nested_path == "/" {
path.into()
} else if path == "/" {
(&*nested_path).into()
} else if let Some(path) = path.strip_suffix('/') {
format!("{}{}", path, nested_path).into()
} else {
format!("{}{}", path, nested_path).into()
};
self = match route {
Endpoint::MethodRouter(method_router) => self.route(
&full_path,
method_router.layer(layer_fn(|s| StripPrefix::new(s, prefix))),
),
Endpoint::Route(route) => self.route(&full_path, StripPrefix::new(route, prefix)),
};
}

for (id, nested_path) in node.route_id_to_path {
let route = routes.remove(&id).unwrap();
let full_path: Cow<str> = if &*nested_path == "/" {
path.into()
} else if path == "/" {
(&*nested_path).into()
} else if let Some(path) = path.strip_suffix('/') {
format!("{}{}", path, nested_path).into()
} else {
format!("{}{}", path, nested_path).into()
};
self = match route {
Endpoint::MethodRouter(method_router) => self.route(
&full_path,
method_router.layer(layer_fn(|s| StripPrefix::new(s, prefix))),
),
Endpoint::Route(route) => {
self.route(&full_path, StripPrefix::new(route, prefix))
}
};
}
debug_assert!(routes.is_empty());

debug_assert!(routes.is_empty());
}
// otherwise we add a wildcard route to the service
Err(svc) => {
let path = if path.ends_with('/') {
format!("{}*{}", path, NEST_TAIL_PARAM)
} else {
format!("{}/*{}", path, NEST_TAIL_PARAM)
};
self
}

self = self.route(&path, strip_prefix::StripPrefix::new(svc, prefix));
}
/// TODO
pub fn nest_service<T>(mut self, mut path: &str, svc: T) -> Self
where
T: Service<Request<B>, Response = Response, Error = Infallible> + Clone + Send + 'static,
T::Future: Send + 'static,
{
if path.is_empty() {
// nesting at `""` and `"/"` should mean the same thing
path = "/";
}

if path.contains('*') {
panic!("Invalid route: nested routes cannot contain wildcards (*)");
}

let prefix = path;

let path = if path.ends_with('/') {
format!("{}*{}", path, NEST_TAIL_PARAM)
davidpdrsn marked this conversation as resolved.
Show resolved Hide resolved
} else {
format!("{}/*{}", path, NEST_TAIL_PARAM)
davidpdrsn marked this conversation as resolved.
Show resolved Hide resolved
};

let svc = strip_prefix::StripPrefix::new(svc, prefix);
self = self.route(&path, svc.clone());

// `/*rest` is not matched by `/` so we need to also register a router at the
// prefix itself. Otherwise if you were to nest at `/foo` then `/foo` itself
// wouldn't match, which it should
self = self.route(prefix, svc.clone());
// same goes for `/foo/`, that should also match
self = self.route(&format!("{}/", prefix), svc);

self
}

Expand All @@ -259,7 +260,6 @@ where
routes,
node,
fallback,
nested_at_root,
} = other.into();

for (id, route) in routes {
Expand All @@ -282,8 +282,6 @@ where
}
};

self.nested_at_root = self.nested_at_root || nested_at_root;

self
}

Expand Down Expand Up @@ -324,7 +322,6 @@ where
routes,
node: self.node,
fallback,
nested_at_root: self.nested_at_root,
}
}

Expand Down Expand Up @@ -363,7 +360,6 @@ where
routes,
node: self.node,
fallback: self.fallback,
nested_at_root: self.nested_at_root,
}
}

Expand Down Expand Up @@ -419,24 +415,8 @@ where

#[cfg(feature = "matched-path")]
if let Some(matched_path) = self.node.route_id_to_path.get(&id) {
use crate::extract::MatchedPath;

let matched_path = if let Some(previous) = req.extensions_mut().get::<MatchedPath>() {
// a previous `MatchedPath` might exist if we're inside a nested Router
let previous = if let Some(previous) =
previous.as_str().strip_suffix(NEST_TAIL_PARAM_CAPTURE)
{
previous
} else {
previous.as_str()
};

let matched_path = format!("{}{}", previous, matched_path);
matched_path.into()
} else {
Arc::clone(matched_path)
};
req.extensions_mut().insert(MatchedPath(matched_path));
req.extensions_mut()
.insert(MatchedPath(Arc::clone(matched_path)));
davidpdrsn marked this conversation as resolved.
Show resolved Hide resolved
} else {
#[cfg(debug_assertions)]
panic!("should always have a matched path for a route id");
Expand All @@ -455,17 +435,6 @@ where
Endpoint::Route(inner) => inner.call(req),
}
}

fn panic_on_matchit_error(&self, err: matchit::InsertError) {
if self.nested_at_root {
panic!(
"Invalid route: {}. Note that `nest(\"/\", _)` conflicts with all routes. Use `Router::fallback` instead",
err,
);
davidpdrsn marked this conversation as resolved.
Show resolved Hide resolved
} else {
panic!("Invalid route: {}", err);
}
}
}

impl<B> Service<Request<B>> for Router<B>
Expand Down