From fba918ebf8a079feeb302a1ed6bbb9c5c5eac57c Mon Sep 17 00:00:00 2001 From: Trezy Date: Thu, 07 May 2026 20:26:10 +0000 Subject: [PATCH] fix: fix base path not being respected by trailing slash redirects Signed-off-by: Trezy --- src/server.rs | 20 +++++++++++++++++++- tests/common/app.rs | 7 +++++++ tests/e2e_base_path.rs | 127 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 file(s) changed, 153 insertion(s)(+), 1 deletion(s)(-) diff --git a/src/server.rs b/src/server.rs --- a/src/server.rs +++ b/src/server.rs @@ -138,9 +138,27 @@ .merge(domain_routes) .fallback_service(serve_dir); let outer = if let Some(ref base_path) = state.config.base_path { + let bp = base_path.clone(); + let rewrite_redirects = + axum::middleware::from_fn(move |req, next: axum::middleware::Next| { + let bp = bp.clone(); + async move { + let mut response: Response = next.run(req).await; + if response.status().is_redirection() + && let Some(loc) = response.headers().get(header::LOCATION) + && let Ok(loc_str) = loc.to_str() + && loc_str.starts_with('/') + && !loc_str.starts_with(&bp) + && let Ok(new_loc) = format!("{}{}", bp, loc_str).parse() + { + response.headers_mut().insert(header::LOCATION, new_loc); + } + response + } + }); Router::new() .route("/health", get(health)) - .nest(base_path, app_routes) + .nest(base_path, app_routes.layer(rewrite_redirects)) } else { Router::new() .route("/health", get(health)) diff --git a/tests/common/app.rs b/tests/common/app.rs --- a/tests/common/app.rs +++ b/tests/common/app.rs @@ -169,6 +169,13 @@ admin_token, } } + pub async fn new_with_base_path(base_path: &str) -> Self { + let mut app = Self::new().await; + app.state.config.base_path = Some(base_path.to_string()); + app.router = server::router(app.state.clone()); + app + } + pub async fn new_with_encryption() -> Self { let mut app = Self::new().await; // Set a test encryption key (32 bytes) diff --git a/tests/e2e_base_path.rs b/tests/e2e_base_path.rs new file mode 100644 --- /dev/null +++ b/tests/e2e_base_path.rs @@ -0,0 +1,127 @@ +mod common; + +use axum::body::Body; +use axum::http::{Request, StatusCode, header}; +use http_body_util::BodyExt; +use serial_test::serial; +use tower::ServiceExt; + +#[tokio::test] +#[serial] +#[ignore] +async fn health_at_root_when_base_path_set() { + let app = common::app::TestApp::new_with_base_path("/hv").await; + + let resp = app + .router + .clone() + .oneshot( + Request::builder() + .uri("/health") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(resp.status(), StatusCode::OK); + let body = resp.into_body().collect().await.unwrap().to_bytes(); + assert_eq!(&body[..], b"ok"); +} + +#[tokio::test] +#[serial] +#[ignore] +async fn health_not_nested_under_base_path() { + let app = common::app::TestApp::new_with_base_path("/hv").await; + + let resp = app + .router + .clone() + .oneshot( + Request::builder() + .uri("/hv/health") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_ne!(resp.status(), StatusCode::OK); +} + +#[tokio::test] +#[serial] +#[ignore] +async fn config_accessible_under_base_path() { + let app = common::app::TestApp::new_with_base_path("/hv").await; + + let resp = app + .router + .clone() + .oneshot( + Request::builder() + .uri("/hv/config") + .header("host", "127.0.0.1") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(resp.status(), StatusCode::OK); +} + +#[tokio::test] +#[serial] +#[ignore] +async fn config_not_at_root_when_base_path_set() { + let app = common::app::TestApp::new_with_base_path("/hv").await; + + let resp = app + .router + .clone() + .oneshot( + Request::builder() + .uri("/config") + .header("host", "127.0.0.1") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_ne!(resp.status(), StatusCode::OK); +} + +#[tokio::test] +#[serial] +#[ignore] +async fn redirect_includes_base_path_prefix() { + let app = common::app::TestApp::new_with_base_path("/hv").await; + + let resp = app + .router + .clone() + .oneshot( + Request::builder() + .uri("/hv/login") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + if resp.status().is_redirection() { + let location = resp + .headers() + .get(header::LOCATION) + .expect("redirect should have Location header") + .to_str() + .unwrap(); + assert!( + location.starts_with("/hv"), + "redirect Location should include base path, got: {location}" + ); + } +} -- tangled.sh