From 1c14d8d4f8a74f3c661e6752ef0edc2b8bc68198 Mon Sep 17 00:00:00 2001 From: firas yangui Date: Fri, 19 Jun 2026 17:09:24 +0200 Subject: [PATCH 1/4] fix: normalize route paths with a leading slash --- cot/src/router.rs | 21 ++++++++++++++++++++- cot/src/router/path.rs | 19 ++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/cot/src/router.rs b/cot/src/router.rs index 4c733e249..2d49c26ad 100644 --- a/cot/src/router.rs +++ b/cot/src/router.rs @@ -527,6 +527,9 @@ pub fn split_view_name(view_name: &str) -> (Option<&str>, &str) { /// A route that can be used to route requests to their respective views. /// +/// Non-empty route paths may omit the leading slash. Cot normalizes them by +/// prepending `/`, so `"home"` and `"/home"` define the same route. +/// /// # Examples /// /// ``` @@ -562,7 +565,8 @@ impl Route { /// # unimplemented!() /// } /// - /// let route = Route::with_handler("/", home); + /// let route = Route::with_handler("home", home); + /// assert_eq!(route.url(), "/home"); /// ``` #[must_use] pub fn with_handler(url: &str, handler: H) -> Self @@ -1130,6 +1134,21 @@ mod tests { assert_eq!(response.status(), StatusCode::OK); } + #[cot::test] + async fn router_route_without_leading_slash() { + let route = Route::with_handler_and_name("test", MockHandler, "test"); + assert_eq!(route.url(), "/test"); + + let router = Router::with_urls(vec![route]); + let response = router.route(test_request(), "/test").await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + + let url = router + .reverse(None, "test", &ReverseParamMap::new()) + .unwrap(); + assert_eq!(url, "/test"); + } + #[cot::test] async fn router_handle() { let route = Route::with_handler("/test", MockHandler); diff --git a/cot/src/router/path.rs b/cot/src/router/path.rs index ba1f6fd67..cd2507992 100644 --- a/cot/src/router/path.rs +++ b/cot/src/router/path.rs @@ -25,7 +25,10 @@ impl PathMatcher { Param { start: usize }, } - let path_pattern = path_pattern.into(); + let mut path_pattern = path_pattern.into(); + if !path_pattern.is_empty() && !path_pattern.starts_with('/') { + path_pattern.insert(0, '/'); + } let mut parts = Vec::new(); let mut state = State::Literal { start: 0 }; @@ -349,6 +352,20 @@ mod tests { assert_eq!(path_parser.capture("/test"), None); } + #[test] + fn path_parser_adds_missing_leading_slash() { + let path_parser = PathMatcher::new("users/{id}"); + let mut params = ReverseParamMap::new(); + params.insert("id", "123"); + + assert_eq!( + path_parser.capture("/users/123"), + Some(CaptureResult::new(vec![PathParam::new("id", "123")], "")) + ); + assert_eq!(path_parser.reverse(¶ms).unwrap(), "/users/123"); + assert_eq!(path_parser.to_string(), "/users/{id}"); + } + #[test] fn path_parser_escaped() { let path_parser = PathMatcher::new("/users/{{{{{{escaped}}}}}}"); From a33aa84be3b80fcb1cdd78f9795a92b471d318d9 Mon Sep 17 00:00:00 2001 From: firas yangui Date: Mon, 22 Jun 2026 09:56:22 +0200 Subject: [PATCH 2/4] ci: skip guide code tests under Miri --- .github/workflows/rust.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 0e2893b7c..ebe3e9f85 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -398,7 +398,8 @@ jobs: run: cargo miri setup - name: Miri test - run: cargo miri nextest run --no-fail-fast --all-features + # Guide code-block tests spawn Cargo, which Miri cannot execute. + run: cargo miri nextest run --no-fail-fast --all-features -E 'not binary(=doc_code_blocks)' env: MIRIFLAGS: -Zmiri-disable-isolation From 35493a14152db93b2e637efb9fc4f4f5080dd520 Mon Sep 17 00:00:00 2001 From: firas yangui Date: Mon, 22 Jun 2026 10:02:44 +0200 Subject: [PATCH 3/4] ci: skip child-process doc tests under Miri --- .github/workflows/rust.yml | 3 +-- cot-test/tests/doc_code_blocks.rs | 5 +++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index ebe3e9f85..0e2893b7c 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -398,8 +398,7 @@ jobs: run: cargo miri setup - name: Miri test - # Guide code-block tests spawn Cargo, which Miri cannot execute. - run: cargo miri nextest run --no-fail-fast --all-features -E 'not binary(=doc_code_blocks)' + run: cargo miri nextest run --no-fail-fast --all-features env: MIRIFLAGS: -Zmiri-disable-isolation diff --git a/cot-test/tests/doc_code_blocks.rs b/cot-test/tests/doc_code_blocks.rs index 7dea66aef..1a5a6c1b0 100644 --- a/cot-test/tests/doc_code_blocks.rs +++ b/cot-test/tests/doc_code_blocks.rs @@ -14,6 +14,11 @@ type TestRunner = fn(&str) -> Result<(), Failed>; static TEST_RUNNERS: OnceLock> = OnceLock::new(); fn main() { + // These tests spawn Cargo, which Miri cannot execute. + if cfg!(miri) { + return; + } + let args = Arguments::from_args(); let mut test_runners: HashMap<(TestLanguage, TestConfig), TestRunner> = HashMap::new(); From 55c1384dfabe9c958ab28540e1af004dda85afc5 Mon Sep 17 00:00:00 2001 From: firas yangui Date: Mon, 22 Jun 2026 10:41:21 +0200 Subject: [PATCH 4/4] revert: leave Miri fix to #593 --- cot-test/tests/doc_code_blocks.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/cot-test/tests/doc_code_blocks.rs b/cot-test/tests/doc_code_blocks.rs index 1a5a6c1b0..7dea66aef 100644 --- a/cot-test/tests/doc_code_blocks.rs +++ b/cot-test/tests/doc_code_blocks.rs @@ -14,11 +14,6 @@ type TestRunner = fn(&str) -> Result<(), Failed>; static TEST_RUNNERS: OnceLock> = OnceLock::new(); fn main() { - // These tests spawn Cargo, which Miri cannot execute. - if cfg!(miri) { - return; - } - let args = Arguments::from_args(); let mut test_runners: HashMap<(TestLanguage, TestConfig), TestRunner> = HashMap::new();