From c293ba3005a2acb87f7e4358bd62f4c84061c2cd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 09:46:28 +0000 Subject: [PATCH 01/37] Initial plan From db5820382f61ec5e3a39d826c7617f2bf82c7aef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 10:00:47 +0000 Subject: [PATCH 02/37] Implement URL redirects for common mistakes and tile format suffixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Created martin/src/srv/redirects.rs module with redirect handlers - Added pluralization redirects (styles→style, sprites→sprite, etc.) - Added tile format suffix redirects (.pbf, .mvt, .mlt) - All redirects use HTTP 301 and preserve query strings - Updated server.rs to register redirects with correct precedence - Tile suffix redirects registered before main tile route - Pluralization redirects registered after main routes Co-authored-by: CommanderStorm <26258709+CommanderStorm@users.noreply.github.com> --- martin/src/srv/mod.rs | 2 + martin/src/srv/redirects.rs | 223 ++++++++++++++++++++++++++++++ martin/src/srv/server.rs | 13 +- martin/tests/redirects_test.rs | 241 +++++++++++++++++++++++++++++++++ 4 files changed, 477 insertions(+), 2 deletions(-) create mode 100644 martin/src/srv/redirects.rs create mode 100644 martin/tests/redirects_test.rs diff --git a/martin/src/srv/mod.rs b/martin/src/srv/mod.rs index 3d1b3719f..5a96fb330 100644 --- a/martin/src/srv/mod.rs +++ b/martin/src/srv/mod.rs @@ -22,3 +22,5 @@ mod styles; #[cfg(all(feature = "unstable-rendering", target_os = "linux"))] mod styles_rendering; + +mod redirects; diff --git a/martin/src/srv/redirects.rs b/martin/src/srv/redirects.rs new file mode 100644 index 000000000..d7f8ab466 --- /dev/null +++ b/martin/src/srv/redirects.rs @@ -0,0 +1,223 @@ +use actix_web::http::header::LOCATION; +use actix_web::web::Path; +use actix_web::{HttpRequest, HttpResponse, route}; +use serde::Deserialize; + +/// Redirect handlers for common pluralization mistakes and tile format suffixes. +/// All redirects use HTTP 301 (Permanent Redirect) and preserve query strings. + +// ============================================================================ +// Pluralization Redirects +// ============================================================================ + +/// Redirect `/styles/{style_id}` to `/style/{style_id}` +#[derive(Deserialize)] +struct StyleRedirectRequest { + style_id: String, +} + +#[route("/styles/{style_id}", method = "GET", method = "HEAD")] +async fn redirect_styles(req: HttpRequest, path: Path) -> HttpResponse { + redirect_with_query(&format!("/style/{}", path.style_id), req.query_string()) +} + +/// Redirect `/sprites/{source_ids}.json` to `/sprite/{source_ids}.json` +#[derive(Deserialize)] +struct SpriteJsonRedirectRequest { + source_ids: String, +} + +#[route("/sprites/{source_ids}.json", method = "GET", method = "HEAD")] +async fn redirect_sprites_json( + req: HttpRequest, + path: Path, +) -> HttpResponse { + redirect_with_query(&format!("/sprite/{}.json", path.source_ids), req.query_string()) +} + +/// Redirect `/sprites/{source_ids}.png` to `/sprite/{source_ids}.png` +#[derive(Deserialize)] +struct SpritePngRedirectRequest { + source_ids: String, +} + +#[route("/sprites/{source_ids}.png", method = "GET", method = "HEAD")] +async fn redirect_sprites_png( + req: HttpRequest, + path: Path, +) -> HttpResponse { + redirect_with_query(&format!("/sprite/{}.png", path.source_ids), req.query_string()) +} + +/// Redirect `/sdf_sprites/{source_ids}.json` to `/sdf_sprite/{source_ids}.json` +#[derive(Deserialize)] +struct SdfSpriteJsonRedirectRequest { + source_ids: String, +} + +#[route("/sdf_sprites/{source_ids}.json", method = "GET", method = "HEAD")] +async fn redirect_sdf_sprites_json( + req: HttpRequest, + path: Path, +) -> HttpResponse { + redirect_with_query( + &format!("/sdf_sprite/{}.json", path.source_ids), + req.query_string(), + ) +} + +/// Redirect `/sdf_sprites/{source_ids}.png` to `/sdf_sprite/{source_ids}.png` +#[derive(Deserialize)] +struct SdfSpritePngRedirectRequest { + source_ids: String, +} + +#[route("/sdf_sprites/{source_ids}.png", method = "GET", method = "HEAD")] +async fn redirect_sdf_sprites_png( + req: HttpRequest, + path: Path, +) -> HttpResponse { + redirect_with_query( + &format!("/sdf_sprite/{}.png", path.source_ids), + req.query_string(), + ) +} + +/// Redirect `/fonts/{fontstack}/{start}-{end}` to `/font/{fontstack}/{start}-{end}` +#[derive(Deserialize)] +struct FontRedirectRequest { + fontstack: String, + start: u32, + end: u32, +} + +#[route("/fonts/{fontstack}/{start}-{end}", method = "GET", method = "HEAD")] +async fn redirect_fonts(req: HttpRequest, path: Path) -> HttpResponse { + redirect_with_query( + &format!("/font/{}/{}-{}", path.fontstack, path.start, path.end), + req.query_string(), + ) +} + +/// Redirect `/tiles/{source_ids}/{z}/{x}/{y}` to `/{source_ids}/{z}/{x}/{y}` +#[derive(Deserialize)] +struct TilesRedirectRequest { + source_ids: String, + z: u8, + x: u32, + y: u32, +} + +#[route("/tiles/{source_ids}/{z}/{x}/{y}", method = "GET", method = "HEAD")] +async fn redirect_tiles(req: HttpRequest, path: Path) -> HttpResponse { + redirect_with_query( + &format!("/{}/{}/{}/{}", path.source_ids, path.z, path.x, path.y), + req.query_string(), + ) +} + +// ============================================================================ +// Tile Format Suffix Redirects +// ============================================================================ + +/// Redirect `/{source_ids}/{z}/{x}/{y}.pbf` to `/{source_ids}/{z}/{x}/{y}` +#[derive(Deserialize)] +struct TilePbfRedirectRequest { + source_ids: String, + z: u8, + x: u32, + y: u32, +} + +#[route("/{source_ids}/{z}/{x}/{y}.pbf", method = "GET", method = "HEAD")] +async fn redirect_tile_pbf(req: HttpRequest, path: Path) -> HttpResponse { + redirect_with_query( + &format!("/{}/{}/{}/{}", path.source_ids, path.z, path.x, path.y), + req.query_string(), + ) +} + +/// Redirect `/{source_ids}/{z}/{x}/{y}.mvt` to `/{source_ids}/{z}/{x}/{y}` +#[derive(Deserialize)] +struct TileMvtRedirectRequest { + source_ids: String, + z: u8, + x: u32, + y: u32, +} + +#[route("/{source_ids}/{z}/{x}/{y}.mvt", method = "GET", method = "HEAD")] +async fn redirect_tile_mvt(req: HttpRequest, path: Path) -> HttpResponse { + redirect_with_query( + &format!("/{}/{}/{}/{}", path.source_ids, path.z, path.x, path.y), + req.query_string(), + ) +} + +/// Redirect `/{source_ids}/{z}/{x}/{y}.mlt` to `/{source_ids}/{z}/{x}/{y}` +#[derive(Deserialize)] +struct TileMltRedirectRequest { + source_ids: String, + z: u8, + x: u32, + y: u32, +} + +#[route("/{source_ids}/{z}/{x}/{y}.mlt", method = "GET", method = "HEAD")] +async fn redirect_tile_mlt(req: HttpRequest, path: Path) -> HttpResponse { + redirect_with_query( + &format!("/{}/{}/{}/{}", path.source_ids, path.z, path.x, path.y), + req.query_string(), + ) +} + +// ============================================================================ +// Helper Functions +// ============================================================================ + +/// Create a 301 Permanent Redirect response, preserving query strings if present +fn redirect_with_query(target_path: &str, query_string: &str) -> HttpResponse { + let location = if query_string.is_empty() { + target_path.to_string() + } else { + format!("{}?{}", target_path, query_string) + }; + + HttpResponse::MovedPermanently() + .insert_header((LOCATION, location)) + .finish() +} + +// ============================================================================ +// Public API for registering redirect routes +// ============================================================================ + +/// Register tile format suffix redirect routes. +/// These MUST be registered BEFORE the main tile route `/{source_ids}/{z}/{x}/{y}` +/// because Actix-Web matches routes in registration order, and more specific +/// patterns need to be registered first. +pub fn register_tile_suffix_redirects(cfg: &mut actix_web::web::ServiceConfig) { + cfg.service(redirect_tile_pbf) + .service(redirect_tile_mvt) + .service(redirect_tile_mlt); +} + +/// Register pluralization redirect routes. +/// These should be registered AFTER main routes to act as fallbacks for common mistakes. +pub fn register_pluralization_redirects(cfg: &mut actix_web::web::ServiceConfig) { + cfg.service(redirect_styles) + .service(redirect_sprites_json) + .service(redirect_sprites_png) + .service(redirect_sdf_sprites_json) + .service(redirect_sdf_sprites_png) + .service(redirect_fonts) + .service(redirect_tiles); +} + +/// Register all redirect routes (for backwards compatibility). +/// Prefer using `register_tile_suffix_redirects` and `register_pluralization_redirects` separately. +#[allow(dead_code)] +pub fn register_redirects(cfg: &mut actix_web::web::ServiceConfig) { + register_tile_suffix_redirects(cfg); + register_pluralization_redirects(cfg); +} diff --git a/martin/src/srv/server.rs b/martin/src/srv/server.rs index e610756b1..d98b31749 100644 --- a/martin/src/srv/server.rs +++ b/martin/src/srv/server.rs @@ -46,8 +46,14 @@ pub fn router(cfg: &mut web::ServiceConfig, #[allow(unused_variables)] usr_cfg: .service(crate::srv::admin::get_catalog); #[cfg(feature = "_tiles")] - cfg.service(crate::srv::tiles::metadata::get_source_info) - .service(crate::srv::tiles::content::get_tile); + { + // Register tile format suffix redirects BEFORE the main tile route + // because Actix-Web matches routes in registration order + crate::srv::redirects::register_tile_suffix_redirects(cfg); + + cfg.service(crate::srv::tiles::metadata::get_source_info) + .service(crate::srv::tiles::content::get_tile); + } #[cfg(feature = "sprites")] cfg.service(crate::srv::sprites::get_sprite_sdf_json) @@ -80,6 +86,9 @@ pub fn router(cfg: &mut web::ServiceConfig, #[allow(unused_variables)] usr_cfg: #[cfg(any(not(feature = "webui"), docsrs))] cfg.service(crate::srv::admin::get_index_no_ui); + + // Register pluralization redirect routes last so they act as fallbacks for common mistakes + crate::srv::redirects::register_pluralization_redirects(cfg); } type Server = Pin>>>; diff --git a/martin/tests/redirects_test.rs b/martin/tests/redirects_test.rs new file mode 100644 index 000000000..e5fe0968b --- /dev/null +++ b/martin/tests/redirects_test.rs @@ -0,0 +1,241 @@ +#![cfg(test)] + +use actix_web::http::StatusCode; +use actix_web::test::{TestRequest, call_service}; +use indoc::indoc; +use martin::config::file::srv::SrvConfig; + +pub mod utils; +pub use utils::*; + +macro_rules! create_app { + ($sources:expr) => {{ + let state = mock_sources(mock_cfg($sources)).await.0; + ::actix_web::test::init_service( + ::actix_web::App::new() + .app_data(actix_web::web::Data::new( + ::martin::srv::Catalog::new(&state).unwrap(), + )) + .app_data(actix_web::web::Data::new(state.tiles)) + .app_data(actix_web::web::Data::new( + ::martin_core::tiles::NO_TILE_CACHE, + )) + .app_data(actix_web::web::Data::new(state.sprites)) + .app_data(actix_web::web::Data::new( + ::martin_core::sprites::NO_SPRITE_CACHE, + )) + .app_data(actix_web::web::Data::new(state.fonts)) + .app_data(actix_web::web::Data::new( + ::martin_core::fonts::NO_FONT_CACHE, + )) + .app_data(actix_web::web::Data::new(state.styles)) + .app_data(actix_web::web::Data::new(SrvConfig::default())) + .configure(|c| ::martin::srv::router(c, &SrvConfig::default())), + ) + .await + }}; +} + +fn test_get(path: &str) -> TestRequest { + TestRequest::get().uri(path) +} + +const CONFIG_WITH_STYLES: &str = indoc! {" + styles: + sources: + test_style: ../tests/fixtures/styles/maplibre_demo.json +"}; + +const CONFIG_WITH_SPRITES: &str = indoc! {" + sprites: + sources: + - ../tests/fixtures/sprites/src1 +"}; + +const CONFIG_WITH_FONTS: &str = indoc! {" + fonts: + sources: + - ../tests/fixtures/fonts +"}; + +// Helper function to check redirect +async fn assert_redirect(path: &str, expected_location: &str, config: &str) { + let app = create_app!(config); + let req = test_get(path).to_request(); + let response = call_service(&app, req).await; + + assert_eq!( + response.status(), + StatusCode::MOVED_PERMANENTLY, + "Expected 301 redirect for path: {}", + path + ); + + let location = response + .headers() + .get("location") + .expect("Location header should be present") + .to_str() + .expect("Location should be valid string"); + + assert_eq!( + location, expected_location, + "Redirect location mismatch for path: {}", + path + ); +} + +// ============================================================================ +// Pluralization Redirect Tests +// ============================================================================ + +#[actix_rt::test] +#[cfg(feature = "styles")] +async fn test_redirect_styles_to_style() { + assert_redirect("/styles/test_style", "/style/test_style", CONFIG_WITH_STYLES).await; +} + +#[actix_rt::test] +#[cfg(feature = "sprites")] +async fn test_redirect_sprites_json_to_sprite() { + assert_redirect( + "/sprites/src1.json", + "/sprite/src1.json", + CONFIG_WITH_SPRITES, + ) + .await; +} + +#[actix_rt::test] +#[cfg(feature = "sprites")] +async fn test_redirect_sprites_png_to_sprite() { + assert_redirect( + "/sprites/src1.png", + "/sprite/src1.png", + CONFIG_WITH_SPRITES, + ) + .await; +} + +#[actix_rt::test] +#[cfg(feature = "sprites")] +async fn test_redirect_sdf_sprites_json_to_sdf_sprite() { + assert_redirect( + "/sdf_sprites/src1.json", + "/sdf_sprite/src1.json", + CONFIG_WITH_SPRITES, + ) + .await; +} + +#[actix_rt::test] +#[cfg(feature = "sprites")] +async fn test_redirect_sdf_sprites_png_to_sdf_sprite() { + assert_redirect( + "/sdf_sprites/src1.png", + "/sdf_sprite/src1.png", + CONFIG_WITH_SPRITES, + ) + .await; +} + +#[actix_rt::test] +#[cfg(feature = "fonts")] +async fn test_redirect_fonts_to_font() { + assert_redirect( + "/fonts/Noto%20Sans/0-255", + "/font/Noto%20Sans/0-255", + CONFIG_WITH_FONTS, + ) + .await; +} + +// ============================================================================ +// Tile Format Suffix Redirect Tests +// ============================================================================ + +#[actix_rt::test] +#[cfg(feature = "mbtiles")] +async fn test_redirect_tile_pbf_suffix() { + let config = indoc! {" + mbtiles: + sources: + world_cities: ../tests/fixtures/mbtiles/world_cities.mbtiles + "}; + assert_redirect("/world_cities/0/0/0.pbf", "/world_cities/0/0/0", config).await; +} + +#[actix_rt::test] +#[cfg(feature = "mbtiles")] +async fn test_redirect_tile_mvt_suffix() { + let config = indoc! {" + mbtiles: + sources: + world_cities: ../tests/fixtures/mbtiles/world_cities.mbtiles + "}; + assert_redirect("/world_cities/0/0/0.mvt", "/world_cities/0/0/0", config).await; +} + +#[actix_rt::test] +#[cfg(feature = "mbtiles")] +async fn test_redirect_tile_mlt_suffix() { + let config = indoc! {" + mbtiles: + sources: + world_cities: ../tests/fixtures/mbtiles/world_cities.mbtiles + "}; + assert_redirect("/world_cities/0/0/0.mlt", "/world_cities/0/0/0", config).await; +} + +#[actix_rt::test] +#[cfg(feature = "mbtiles")] +async fn test_redirect_tiles_prefix() { + let config = indoc! {" + mbtiles: + sources: + world_cities: ../tests/fixtures/mbtiles/world_cities.mbtiles + "}; + assert_redirect("/tiles/world_cities/0/0/0", "/world_cities/0/0/0", config).await; +} + +// ============================================================================ +// Query String Preservation Tests +// ============================================================================ + +#[actix_rt::test] +#[cfg(feature = "styles")] +async fn test_redirect_preserves_query_string() { + assert_redirect( + "/styles/test_style?version=1.0", + "/style/test_style?version=1.0", + CONFIG_WITH_STYLES, + ) + .await; +} + +#[actix_rt::test] +#[cfg(feature = "sprites")] +async fn test_redirect_sprites_preserves_query_string() { + assert_redirect( + "/sprites/src1.json?scale=2", + "/sprite/src1.json?scale=2", + CONFIG_WITH_SPRITES, + ) + .await; +} + +#[actix_rt::test] +#[cfg(feature = "mbtiles")] +async fn test_redirect_tile_preserves_query_string() { + let config = indoc! {" + mbtiles: + sources: + world_cities: ../tests/fixtures/mbtiles/world_cities.mbtiles + "}; + assert_redirect( + "/world_cities/0/0/0.pbf?format=json", + "/world_cities/0/0/0?format=json", + config, + ) + .await; +} From d5a0cb5ebf7546450eb7de8f1ce9bdfc6a1b1cc5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 10:02:39 +0000 Subject: [PATCH 03/37] chore(fmt): apply pre-commit formatting fixes --- martin/src/srv/redirects.rs | 10 ++++++++-- martin/src/srv/server.rs | 2 +- martin/tests/redirects_test.rs | 14 +++++++------- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/martin/src/srv/redirects.rs b/martin/src/srv/redirects.rs index d7f8ab466..b6396ee72 100644 --- a/martin/src/srv/redirects.rs +++ b/martin/src/srv/redirects.rs @@ -32,7 +32,10 @@ async fn redirect_sprites_json( req: HttpRequest, path: Path, ) -> HttpResponse { - redirect_with_query(&format!("/sprite/{}.json", path.source_ids), req.query_string()) + redirect_with_query( + &format!("/sprite/{}.json", path.source_ids), + req.query_string(), + ) } /// Redirect `/sprites/{source_ids}.png` to `/sprite/{source_ids}.png` @@ -46,7 +49,10 @@ async fn redirect_sprites_png( req: HttpRequest, path: Path, ) -> HttpResponse { - redirect_with_query(&format!("/sprite/{}.png", path.source_ids), req.query_string()) + redirect_with_query( + &format!("/sprite/{}.png", path.source_ids), + req.query_string(), + ) } /// Redirect `/sdf_sprites/{source_ids}.json` to `/sdf_sprite/{source_ids}.json` diff --git a/martin/src/srv/server.rs b/martin/src/srv/server.rs index d98b31749..8ec34337b 100644 --- a/martin/src/srv/server.rs +++ b/martin/src/srv/server.rs @@ -50,7 +50,7 @@ pub fn router(cfg: &mut web::ServiceConfig, #[allow(unused_variables)] usr_cfg: // Register tile format suffix redirects BEFORE the main tile route // because Actix-Web matches routes in registration order crate::srv::redirects::register_tile_suffix_redirects(cfg); - + cfg.service(crate::srv::tiles::metadata::get_source_info) .service(crate::srv::tiles::content::get_tile); } diff --git a/martin/tests/redirects_test.rs b/martin/tests/redirects_test.rs index e5fe0968b..fe1cd1e48 100644 --- a/martin/tests/redirects_test.rs +++ b/martin/tests/redirects_test.rs @@ -92,7 +92,12 @@ async fn assert_redirect(path: &str, expected_location: &str, config: &str) { #[actix_rt::test] #[cfg(feature = "styles")] async fn test_redirect_styles_to_style() { - assert_redirect("/styles/test_style", "/style/test_style", CONFIG_WITH_STYLES).await; + assert_redirect( + "/styles/test_style", + "/style/test_style", + CONFIG_WITH_STYLES, + ) + .await; } #[actix_rt::test] @@ -109,12 +114,7 @@ async fn test_redirect_sprites_json_to_sprite() { #[actix_rt::test] #[cfg(feature = "sprites")] async fn test_redirect_sprites_png_to_sprite() { - assert_redirect( - "/sprites/src1.png", - "/sprite/src1.png", - CONFIG_WITH_SPRITES, - ) - .await; + assert_redirect("/sprites/src1.png", "/sprite/src1.png", CONFIG_WITH_SPRITES).await; } #[actix_rt::test] From 0fe88682831161f4264504024912149c84882c3c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 10:05:49 +0000 Subject: [PATCH 04/37] Fix clippy warnings in redirects module Co-authored-by: CommanderStorm <26258709+CommanderStorm@users.noreply.github.com> --- martin/src/srv/redirects.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/martin/src/srv/redirects.rs b/martin/src/srv/redirects.rs index b6396ee72..d061bf566 100644 --- a/martin/src/srv/redirects.rs +++ b/martin/src/srv/redirects.rs @@ -3,12 +3,11 @@ use actix_web::web::Path; use actix_web::{HttpRequest, HttpResponse, route}; use serde::Deserialize; -/// Redirect handlers for common pluralization mistakes and tile format suffixes. -/// All redirects use HTTP 301 (Permanent Redirect) and preserve query strings. - // ============================================================================ // Pluralization Redirects // ============================================================================ +// Redirect handlers for common pluralization mistakes and tile format suffixes. +// All redirects use HTTP 301 (Permanent Redirect) and preserve query strings. /// Redirect `/styles/{style_id}` to `/style/{style_id}` #[derive(Deserialize)] @@ -186,7 +185,7 @@ fn redirect_with_query(target_path: &str, query_string: &str) -> HttpResponse { let location = if query_string.is_empty() { target_path.to_string() } else { - format!("{}?{}", target_path, query_string) + format!("{target_path}?{query_string}") }; HttpResponse::MovedPermanently() From 3eab04b6d6f1f5035876607066f5242dfdd70a7d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 10:13:02 +0000 Subject: [PATCH 05/37] Refactor: co-locate redirects with their target handlers - Moved redirect handlers from separate redirects.rs to their respective modules - Styles redirects now in styles.rs - Sprites redirects now in sprites.rs - Fonts redirects now in fonts.rs - Tile redirects now in tiles/content.rs - Updated server.rs to register handlers from their new locations - Removed redirects.rs module - Removed test file - Simpler code organization - redirects next to their targets Co-authored-by: CommanderStorm <26258709+CommanderStorm@users.noreply.github.com> --- martin/src/srv/fonts.rs | 17 ++- martin/src/srv/mod.rs | 2 - martin/src/srv/redirects.rs | 228 ------------------------------ martin/src/srv/server.rs | 40 ++++-- martin/src/srv/sprites.rs | 49 ++++++- martin/src/srv/styles.rs | 19 ++- martin/src/srv/tiles/content.rs | 50 ++++++- martin/tests/redirects_test.rs | 241 -------------------------------- 8 files changed, 158 insertions(+), 488 deletions(-) delete mode 100644 martin/src/srv/redirects.rs delete mode 100644 martin/tests/redirects_test.rs diff --git a/martin/src/srv/fonts.rs b/martin/src/srv/fonts.rs index f4e35216b..c86f9b869 100644 --- a/martin/src/srv/fonts.rs +++ b/martin/src/srv/fonts.rs @@ -2,9 +2,10 @@ use std::string::ToString; use actix_middleware_etag::Etag; use actix_web::error::{ErrorBadRequest, ErrorNotFound}; +use actix_web::http::header::LOCATION; use actix_web::middleware::Compress; use actix_web::web::{Data, Path}; -use actix_web::{HttpResponse, Result as ActixResult, route}; +use actix_web::{HttpRequest, HttpResponse, Result as ActixResult, route}; use martin_core::fonts::{FontError, FontSources, OptFontCache}; use serde::Deserialize; @@ -43,6 +44,20 @@ async fn get_font( .body(data)) } +/// Redirect `/fonts/{fontstack}/{start}-{end}` to `/font/{fontstack}/{start}-{end}` (HTTP 301) +#[route("/fonts/{fontstack}/{start}-{end}", method = "GET", method = "HEAD")] +pub async fn redirect_fonts(req: HttpRequest, path: Path) -> HttpResponse { + let location = format!("/font/{}/{}-{}", path.fontstack, path.start, path.end); + let location = if req.query_string().is_empty() { + location + } else { + format!("{location}?{}", req.query_string()) + }; + HttpResponse::MovedPermanently() + .insert_header((LOCATION, location)) + .finish() +} + pub fn map_font_error(e: FontError) -> actix_web::Error { match e { FontError::FontNotFound(_) => ErrorNotFound(e.to_string()), diff --git a/martin/src/srv/mod.rs b/martin/src/srv/mod.rs index 5a96fb330..3d1b3719f 100644 --- a/martin/src/srv/mod.rs +++ b/martin/src/srv/mod.rs @@ -22,5 +22,3 @@ mod styles; #[cfg(all(feature = "unstable-rendering", target_os = "linux"))] mod styles_rendering; - -mod redirects; diff --git a/martin/src/srv/redirects.rs b/martin/src/srv/redirects.rs deleted file mode 100644 index d061bf566..000000000 --- a/martin/src/srv/redirects.rs +++ /dev/null @@ -1,228 +0,0 @@ -use actix_web::http::header::LOCATION; -use actix_web::web::Path; -use actix_web::{HttpRequest, HttpResponse, route}; -use serde::Deserialize; - -// ============================================================================ -// Pluralization Redirects -// ============================================================================ -// Redirect handlers for common pluralization mistakes and tile format suffixes. -// All redirects use HTTP 301 (Permanent Redirect) and preserve query strings. - -/// Redirect `/styles/{style_id}` to `/style/{style_id}` -#[derive(Deserialize)] -struct StyleRedirectRequest { - style_id: String, -} - -#[route("/styles/{style_id}", method = "GET", method = "HEAD")] -async fn redirect_styles(req: HttpRequest, path: Path) -> HttpResponse { - redirect_with_query(&format!("/style/{}", path.style_id), req.query_string()) -} - -/// Redirect `/sprites/{source_ids}.json` to `/sprite/{source_ids}.json` -#[derive(Deserialize)] -struct SpriteJsonRedirectRequest { - source_ids: String, -} - -#[route("/sprites/{source_ids}.json", method = "GET", method = "HEAD")] -async fn redirect_sprites_json( - req: HttpRequest, - path: Path, -) -> HttpResponse { - redirect_with_query( - &format!("/sprite/{}.json", path.source_ids), - req.query_string(), - ) -} - -/// Redirect `/sprites/{source_ids}.png` to `/sprite/{source_ids}.png` -#[derive(Deserialize)] -struct SpritePngRedirectRequest { - source_ids: String, -} - -#[route("/sprites/{source_ids}.png", method = "GET", method = "HEAD")] -async fn redirect_sprites_png( - req: HttpRequest, - path: Path, -) -> HttpResponse { - redirect_with_query( - &format!("/sprite/{}.png", path.source_ids), - req.query_string(), - ) -} - -/// Redirect `/sdf_sprites/{source_ids}.json` to `/sdf_sprite/{source_ids}.json` -#[derive(Deserialize)] -struct SdfSpriteJsonRedirectRequest { - source_ids: String, -} - -#[route("/sdf_sprites/{source_ids}.json", method = "GET", method = "HEAD")] -async fn redirect_sdf_sprites_json( - req: HttpRequest, - path: Path, -) -> HttpResponse { - redirect_with_query( - &format!("/sdf_sprite/{}.json", path.source_ids), - req.query_string(), - ) -} - -/// Redirect `/sdf_sprites/{source_ids}.png` to `/sdf_sprite/{source_ids}.png` -#[derive(Deserialize)] -struct SdfSpritePngRedirectRequest { - source_ids: String, -} - -#[route("/sdf_sprites/{source_ids}.png", method = "GET", method = "HEAD")] -async fn redirect_sdf_sprites_png( - req: HttpRequest, - path: Path, -) -> HttpResponse { - redirect_with_query( - &format!("/sdf_sprite/{}.png", path.source_ids), - req.query_string(), - ) -} - -/// Redirect `/fonts/{fontstack}/{start}-{end}` to `/font/{fontstack}/{start}-{end}` -#[derive(Deserialize)] -struct FontRedirectRequest { - fontstack: String, - start: u32, - end: u32, -} - -#[route("/fonts/{fontstack}/{start}-{end}", method = "GET", method = "HEAD")] -async fn redirect_fonts(req: HttpRequest, path: Path) -> HttpResponse { - redirect_with_query( - &format!("/font/{}/{}-{}", path.fontstack, path.start, path.end), - req.query_string(), - ) -} - -/// Redirect `/tiles/{source_ids}/{z}/{x}/{y}` to `/{source_ids}/{z}/{x}/{y}` -#[derive(Deserialize)] -struct TilesRedirectRequest { - source_ids: String, - z: u8, - x: u32, - y: u32, -} - -#[route("/tiles/{source_ids}/{z}/{x}/{y}", method = "GET", method = "HEAD")] -async fn redirect_tiles(req: HttpRequest, path: Path) -> HttpResponse { - redirect_with_query( - &format!("/{}/{}/{}/{}", path.source_ids, path.z, path.x, path.y), - req.query_string(), - ) -} - -// ============================================================================ -// Tile Format Suffix Redirects -// ============================================================================ - -/// Redirect `/{source_ids}/{z}/{x}/{y}.pbf` to `/{source_ids}/{z}/{x}/{y}` -#[derive(Deserialize)] -struct TilePbfRedirectRequest { - source_ids: String, - z: u8, - x: u32, - y: u32, -} - -#[route("/{source_ids}/{z}/{x}/{y}.pbf", method = "GET", method = "HEAD")] -async fn redirect_tile_pbf(req: HttpRequest, path: Path) -> HttpResponse { - redirect_with_query( - &format!("/{}/{}/{}/{}", path.source_ids, path.z, path.x, path.y), - req.query_string(), - ) -} - -/// Redirect `/{source_ids}/{z}/{x}/{y}.mvt` to `/{source_ids}/{z}/{x}/{y}` -#[derive(Deserialize)] -struct TileMvtRedirectRequest { - source_ids: String, - z: u8, - x: u32, - y: u32, -} - -#[route("/{source_ids}/{z}/{x}/{y}.mvt", method = "GET", method = "HEAD")] -async fn redirect_tile_mvt(req: HttpRequest, path: Path) -> HttpResponse { - redirect_with_query( - &format!("/{}/{}/{}/{}", path.source_ids, path.z, path.x, path.y), - req.query_string(), - ) -} - -/// Redirect `/{source_ids}/{z}/{x}/{y}.mlt` to `/{source_ids}/{z}/{x}/{y}` -#[derive(Deserialize)] -struct TileMltRedirectRequest { - source_ids: String, - z: u8, - x: u32, - y: u32, -} - -#[route("/{source_ids}/{z}/{x}/{y}.mlt", method = "GET", method = "HEAD")] -async fn redirect_tile_mlt(req: HttpRequest, path: Path) -> HttpResponse { - redirect_with_query( - &format!("/{}/{}/{}/{}", path.source_ids, path.z, path.x, path.y), - req.query_string(), - ) -} - -// ============================================================================ -// Helper Functions -// ============================================================================ - -/// Create a 301 Permanent Redirect response, preserving query strings if present -fn redirect_with_query(target_path: &str, query_string: &str) -> HttpResponse { - let location = if query_string.is_empty() { - target_path.to_string() - } else { - format!("{target_path}?{query_string}") - }; - - HttpResponse::MovedPermanently() - .insert_header((LOCATION, location)) - .finish() -} - -// ============================================================================ -// Public API for registering redirect routes -// ============================================================================ - -/// Register tile format suffix redirect routes. -/// These MUST be registered BEFORE the main tile route `/{source_ids}/{z}/{x}/{y}` -/// because Actix-Web matches routes in registration order, and more specific -/// patterns need to be registered first. -pub fn register_tile_suffix_redirects(cfg: &mut actix_web::web::ServiceConfig) { - cfg.service(redirect_tile_pbf) - .service(redirect_tile_mvt) - .service(redirect_tile_mlt); -} - -/// Register pluralization redirect routes. -/// These should be registered AFTER main routes to act as fallbacks for common mistakes. -pub fn register_pluralization_redirects(cfg: &mut actix_web::web::ServiceConfig) { - cfg.service(redirect_styles) - .service(redirect_sprites_json) - .service(redirect_sprites_png) - .service(redirect_sdf_sprites_json) - .service(redirect_sdf_sprites_png) - .service(redirect_fonts) - .service(redirect_tiles); -} - -/// Register all redirect routes (for backwards compatibility). -/// Prefer using `register_tile_suffix_redirects` and `register_pluralization_redirects` separately. -#[allow(dead_code)] -pub fn register_redirects(cfg: &mut actix_web::web::ServiceConfig) { - register_tile_suffix_redirects(cfg); - register_pluralization_redirects(cfg); -} diff --git a/martin/src/srv/server.rs b/martin/src/srv/server.rs index d6c5697bf..68de99606 100644 --- a/martin/src/srv/server.rs +++ b/martin/src/srv/server.rs @@ -59,23 +59,46 @@ fn register_services(cfg: &mut web::ServiceConfig, #[allow(unused_variables)] us { // Register tile format suffix redirects BEFORE the main tile route // because Actix-Web matches routes in registration order - crate::srv::redirects::register_tile_suffix_redirects(cfg); + cfg.service(crate::srv::tiles::content::redirect_tile_pbf) + .service(crate::srv::tiles::content::redirect_tile_mvt) + .service(crate::srv::tiles::content::redirect_tile_mlt); cfg.service(crate::srv::tiles::metadata::get_source_info) .service(crate::srv::tiles::content::get_tile); + + // Register /tiles/ prefix redirect after main tile route + cfg.service(crate::srv::tiles::content::redirect_tiles); } #[cfg(feature = "sprites")] - cfg.service(crate::srv::sprites::get_sprite_sdf_json) - .service(crate::srv::sprites::get_sprite_json) - .service(crate::srv::sprites::get_sprite_sdf_png) - .service(crate::srv::sprites::get_sprite_png); + { + cfg.service(crate::srv::sprites::get_sprite_sdf_json) + .service(crate::srv::sprites::get_sprite_json) + .service(crate::srv::sprites::get_sprite_sdf_png) + .service(crate::srv::sprites::get_sprite_png); + + // Register sprite plural redirects + cfg.service(crate::srv::sprites::redirect_sprites_json) + .service(crate::srv::sprites::redirect_sprites_png) + .service(crate::srv::sprites::redirect_sdf_sprites_json) + .service(crate::srv::sprites::redirect_sdf_sprites_png); + } #[cfg(feature = "fonts")] - cfg.service(crate::srv::fonts::get_font); + { + cfg.service(crate::srv::fonts::get_font); + + // Register fonts plural redirect + cfg.service(crate::srv::fonts::redirect_fonts); + } #[cfg(feature = "styles")] - cfg.service(crate::srv::styles::get_style_json); + { + cfg.service(crate::srv::styles::get_style_json); + + // Register styles plural redirect + cfg.service(crate::srv::styles::redirect_styles); + } #[cfg(all(feature = "unstable-rendering", target_os = "linux"))] cfg.service(crate::srv::styles_rendering::get_style_rendered); @@ -96,9 +119,6 @@ fn register_services(cfg: &mut web::ServiceConfig, #[allow(unused_variables)] us #[cfg(any(not(feature = "webui"), docsrs))] cfg.service(crate::srv::admin::get_index_no_ui); - - // Register pluralization redirect routes last so they act as fallbacks for common mistakes - crate::srv::redirects::register_pluralization_redirects(cfg); } type Server = Pin>>>; diff --git a/martin/src/srv/sprites.rs b/martin/src/srv/sprites.rs index 52c122d57..c629be794 100644 --- a/martin/src/srv/sprites.rs +++ b/martin/src/srv/sprites.rs @@ -2,10 +2,10 @@ use std::string::ToString; use actix_middleware_etag::Etag; use actix_web::error::ErrorNotFound; -use actix_web::http::header::ContentType; +use actix_web::http::header::{ContentType, LOCATION}; use actix_web::middleware::Compress; use actix_web::web::{Bytes, Data, Path}; -use actix_web::{HttpResponse, Result as ActixResult, route}; +use actix_web::{HttpRequest, HttpResponse, Result as ActixResult, route}; use martin_core::sprites::{OptSpriteCache, SpriteError, SpriteSources}; use serde::Deserialize; @@ -42,6 +42,12 @@ async fn get_sprite_png( .body(png)) } +/// Redirect `/sprites/{source_ids}.png` to `/sprite/{source_ids}.png` (HTTP 301) +#[route("/sprites/{source_ids}.png", method = "GET", method = "HEAD")] +pub async fn redirect_sprites_png(req: HttpRequest, path: Path) -> HttpResponse { + redirect_with_query(&format!("/sprite/{}.png", path.source_ids), req.query_string()) +} + #[route( "/sdf_sprite/{source_ids}.png", method = "GET", @@ -68,6 +74,15 @@ async fn get_sprite_sdf_png( .body(png)) } +/// Redirect `/sdf_sprites/{source_ids}.png` to `/sdf_sprite/{source_ids}.png` (HTTP 301) +#[route("/sdf_sprites/{source_ids}.png", method = "GET", method = "HEAD")] +pub async fn redirect_sdf_sprites_png(req: HttpRequest, path: Path) -> HttpResponse { + redirect_with_query( + &format!("/sdf_sprite/{}.png", path.source_ids), + req.query_string(), + ) +} + #[route( "/sprite/{source_ids}.json", method = "GET", @@ -95,6 +110,15 @@ async fn get_sprite_json( .body(json)) } +/// Redirect `/sprites/{source_ids}.json` to `/sprite/{source_ids}.json` (HTTP 301) +#[route("/sprites/{source_ids}.json", method = "GET", method = "HEAD")] +pub async fn redirect_sprites_json(req: HttpRequest, path: Path) -> HttpResponse { + redirect_with_query( + &format!("/sprite/{}.json", path.source_ids), + req.query_string(), + ) +} + #[route( "/sdf_sprite/{source_ids}.json", method = "GET", @@ -122,6 +146,15 @@ async fn get_sprite_sdf_json( .body(json)) } +/// Redirect `/sdf_sprites/{source_ids}.json` to `/sdf_sprite/{source_ids}.json` (HTTP 301) +#[route("/sdf_sprites/{source_ids}.json", method = "GET", method = "HEAD")] +pub async fn redirect_sdf_sprites_json(req: HttpRequest, path: Path) -> HttpResponse { + redirect_with_query( + &format!("/sdf_sprite/{}.json", path.source_ids), + req.query_string(), + ) +} + async fn get_sprite(source_ids: &str, sprites: &SpriteSources, as_sdf: bool) -> ActixResult { let sheet = sprites .get_sprites(source_ids, as_sdf) @@ -145,3 +178,15 @@ async fn get_index(source_ids: &str, sprites: &SpriteSources, as_sdf: bool) -> A let json = serde_json::to_vec(&sheet.get_index()).map_err(map_internal_error)?; Ok(Bytes::from(json)) } + +/// Helper function to create a 301 redirect with query string preservation +fn redirect_with_query(target_path: &str, query_string: &str) -> HttpResponse { + let location = if query_string.is_empty() { + target_path.to_string() + } else { + format!("{target_path}?{query_string}") + }; + HttpResponse::MovedPermanently() + .insert_header((LOCATION, location)) + .finish() +} diff --git a/martin/src/srv/styles.rs b/martin/src/srv/styles.rs index 5e125840c..b33f15c37 100644 --- a/martin/src/srv/styles.rs +++ b/martin/src/srv/styles.rs @@ -1,8 +1,8 @@ use actix_middleware_etag::Etag; -use actix_web::http::header::ContentType; +use actix_web::http::header::{ContentType, LOCATION}; use actix_web::middleware::Compress; use actix_web::web::{Data, Path}; -use actix_web::{HttpResponse, route}; +use actix_web::{HttpRequest, HttpResponse, route}; use martin_core::styles::StyleSources; use serde::Deserialize; use tracing::error; @@ -48,3 +48,18 @@ async fn get_style_json(path: Path, styles: Data) -> } } } + +/// Redirect `/styles/{style_id}` to `/style/{style_id}` (HTTP 301) +/// This handles common pluralization mistakes +#[route("/styles/{style_id}", method = "GET", method = "HEAD")] +pub async fn redirect_styles(req: HttpRequest, path: Path) -> HttpResponse { + let location = format!("/style/{}", path.style_id); + let location = if req.query_string().is_empty() { + location + } else { + format!("{location}?{}", req.query_string()) + }; + HttpResponse::MovedPermanently() + .insert_header((LOCATION, location)) + .finish() +} diff --git a/martin/src/srv/tiles/content.rs b/martin/src/srv/tiles/content.rs index 305ed3944..bf10e086e 100644 --- a/martin/src/srv/tiles/content.rs +++ b/martin/src/srv/tiles/content.rs @@ -2,8 +2,8 @@ use actix_http::ContentEncoding; use actix_http::header::Quality; use actix_web::error::{ErrorBadRequest, ErrorNotAcceptable, ErrorNotFound}; use actix_web::http::header::{ - AcceptEncoding, CONTENT_ENCODING, ETAG, Encoding as HeaderEnc, EntityTag, IfNoneMatch, - Preference, + AcceptEncoding, CONTENT_ENCODING, ETAG, LOCATION, Encoding as HeaderEnc, EntityTag, + IfNoneMatch, Preference, }; use actix_web::web::{Data, Path, Query}; use actix_web::{HttpMessage, HttpRequest, HttpResponse, Result as ActixResult, route}; @@ -61,6 +61,52 @@ async fn get_tile( .await } +/// Redirect `/{source_ids}/{z}/{x}/{y}.pbf` to `/{source_ids}/{z}/{x}/{y}` (HTTP 301) +/// Registered before main tile route to match more specific pattern first +#[route("/{source_ids}/{z}/{x}/{y}.pbf", method = "GET", method = "HEAD")] +pub async fn redirect_tile_pbf(req: HttpRequest, path: Path) -> HttpResponse { + redirect_tile_with_query(&path.source_ids, path.z, path.x, path.y, req.query_string()) +} + +/// Redirect `/{source_ids}/{z}/{x}/{y}.mvt` to `/{source_ids}/{z}/{x}/{y}` (HTTP 301) +/// Registered before main tile route to match more specific pattern first +#[route("/{source_ids}/{z}/{x}/{y}.mvt", method = "GET", method = "HEAD")] +pub async fn redirect_tile_mvt(req: HttpRequest, path: Path) -> HttpResponse { + redirect_tile_with_query(&path.source_ids, path.z, path.x, path.y, req.query_string()) +} + +/// Redirect `/{source_ids}/{z}/{x}/{y}.mlt` to `/{source_ids}/{z}/{x}/{y}` (HTTP 301) +/// Registered before main tile route to match more specific pattern first +#[route("/{source_ids}/{z}/{x}/{y}.mlt", method = "GET", method = "HEAD")] +pub async fn redirect_tile_mlt(req: HttpRequest, path: Path) -> HttpResponse { + redirect_tile_with_query(&path.source_ids, path.z, path.x, path.y, req.query_string()) +} + +/// Redirect `/tiles/{source_ids}/{z}/{x}/{y}` to `/{source_ids}/{z}/{x}/{y}` (HTTP 301) +#[route("/tiles/{source_ids}/{z}/{x}/{y}", method = "GET", method = "HEAD")] +pub async fn redirect_tiles(req: HttpRequest, path: Path) -> HttpResponse { + redirect_tile_with_query(&path.source_ids, path.z, path.x, path.y, req.query_string()) +} + +/// Helper function to create a 301 redirect for tiles with query string preservation +fn redirect_tile_with_query( + source_ids: &str, + z: u8, + x: u32, + y: u32, + query_string: &str, +) -> HttpResponse { + let location = format!("/{source_ids}/{z}/{x}/{y}"); + let location = if query_string.is_empty() { + location + } else { + format!("{location}?{query_string}") + }; + HttpResponse::MovedPermanently() + .insert_header((LOCATION, location)) + .finish() +} + pub struct DynTileSource<'a> { pub sources: Vec, pub info: TileInfo, diff --git a/martin/tests/redirects_test.rs b/martin/tests/redirects_test.rs deleted file mode 100644 index fe1cd1e48..000000000 --- a/martin/tests/redirects_test.rs +++ /dev/null @@ -1,241 +0,0 @@ -#![cfg(test)] - -use actix_web::http::StatusCode; -use actix_web::test::{TestRequest, call_service}; -use indoc::indoc; -use martin::config::file::srv::SrvConfig; - -pub mod utils; -pub use utils::*; - -macro_rules! create_app { - ($sources:expr) => {{ - let state = mock_sources(mock_cfg($sources)).await.0; - ::actix_web::test::init_service( - ::actix_web::App::new() - .app_data(actix_web::web::Data::new( - ::martin::srv::Catalog::new(&state).unwrap(), - )) - .app_data(actix_web::web::Data::new(state.tiles)) - .app_data(actix_web::web::Data::new( - ::martin_core::tiles::NO_TILE_CACHE, - )) - .app_data(actix_web::web::Data::new(state.sprites)) - .app_data(actix_web::web::Data::new( - ::martin_core::sprites::NO_SPRITE_CACHE, - )) - .app_data(actix_web::web::Data::new(state.fonts)) - .app_data(actix_web::web::Data::new( - ::martin_core::fonts::NO_FONT_CACHE, - )) - .app_data(actix_web::web::Data::new(state.styles)) - .app_data(actix_web::web::Data::new(SrvConfig::default())) - .configure(|c| ::martin::srv::router(c, &SrvConfig::default())), - ) - .await - }}; -} - -fn test_get(path: &str) -> TestRequest { - TestRequest::get().uri(path) -} - -const CONFIG_WITH_STYLES: &str = indoc! {" - styles: - sources: - test_style: ../tests/fixtures/styles/maplibre_demo.json -"}; - -const CONFIG_WITH_SPRITES: &str = indoc! {" - sprites: - sources: - - ../tests/fixtures/sprites/src1 -"}; - -const CONFIG_WITH_FONTS: &str = indoc! {" - fonts: - sources: - - ../tests/fixtures/fonts -"}; - -// Helper function to check redirect -async fn assert_redirect(path: &str, expected_location: &str, config: &str) { - let app = create_app!(config); - let req = test_get(path).to_request(); - let response = call_service(&app, req).await; - - assert_eq!( - response.status(), - StatusCode::MOVED_PERMANENTLY, - "Expected 301 redirect for path: {}", - path - ); - - let location = response - .headers() - .get("location") - .expect("Location header should be present") - .to_str() - .expect("Location should be valid string"); - - assert_eq!( - location, expected_location, - "Redirect location mismatch for path: {}", - path - ); -} - -// ============================================================================ -// Pluralization Redirect Tests -// ============================================================================ - -#[actix_rt::test] -#[cfg(feature = "styles")] -async fn test_redirect_styles_to_style() { - assert_redirect( - "/styles/test_style", - "/style/test_style", - CONFIG_WITH_STYLES, - ) - .await; -} - -#[actix_rt::test] -#[cfg(feature = "sprites")] -async fn test_redirect_sprites_json_to_sprite() { - assert_redirect( - "/sprites/src1.json", - "/sprite/src1.json", - CONFIG_WITH_SPRITES, - ) - .await; -} - -#[actix_rt::test] -#[cfg(feature = "sprites")] -async fn test_redirect_sprites_png_to_sprite() { - assert_redirect("/sprites/src1.png", "/sprite/src1.png", CONFIG_WITH_SPRITES).await; -} - -#[actix_rt::test] -#[cfg(feature = "sprites")] -async fn test_redirect_sdf_sprites_json_to_sdf_sprite() { - assert_redirect( - "/sdf_sprites/src1.json", - "/sdf_sprite/src1.json", - CONFIG_WITH_SPRITES, - ) - .await; -} - -#[actix_rt::test] -#[cfg(feature = "sprites")] -async fn test_redirect_sdf_sprites_png_to_sdf_sprite() { - assert_redirect( - "/sdf_sprites/src1.png", - "/sdf_sprite/src1.png", - CONFIG_WITH_SPRITES, - ) - .await; -} - -#[actix_rt::test] -#[cfg(feature = "fonts")] -async fn test_redirect_fonts_to_font() { - assert_redirect( - "/fonts/Noto%20Sans/0-255", - "/font/Noto%20Sans/0-255", - CONFIG_WITH_FONTS, - ) - .await; -} - -// ============================================================================ -// Tile Format Suffix Redirect Tests -// ============================================================================ - -#[actix_rt::test] -#[cfg(feature = "mbtiles")] -async fn test_redirect_tile_pbf_suffix() { - let config = indoc! {" - mbtiles: - sources: - world_cities: ../tests/fixtures/mbtiles/world_cities.mbtiles - "}; - assert_redirect("/world_cities/0/0/0.pbf", "/world_cities/0/0/0", config).await; -} - -#[actix_rt::test] -#[cfg(feature = "mbtiles")] -async fn test_redirect_tile_mvt_suffix() { - let config = indoc! {" - mbtiles: - sources: - world_cities: ../tests/fixtures/mbtiles/world_cities.mbtiles - "}; - assert_redirect("/world_cities/0/0/0.mvt", "/world_cities/0/0/0", config).await; -} - -#[actix_rt::test] -#[cfg(feature = "mbtiles")] -async fn test_redirect_tile_mlt_suffix() { - let config = indoc! {" - mbtiles: - sources: - world_cities: ../tests/fixtures/mbtiles/world_cities.mbtiles - "}; - assert_redirect("/world_cities/0/0/0.mlt", "/world_cities/0/0/0", config).await; -} - -#[actix_rt::test] -#[cfg(feature = "mbtiles")] -async fn test_redirect_tiles_prefix() { - let config = indoc! {" - mbtiles: - sources: - world_cities: ../tests/fixtures/mbtiles/world_cities.mbtiles - "}; - assert_redirect("/tiles/world_cities/0/0/0", "/world_cities/0/0/0", config).await; -} - -// ============================================================================ -// Query String Preservation Tests -// ============================================================================ - -#[actix_rt::test] -#[cfg(feature = "styles")] -async fn test_redirect_preserves_query_string() { - assert_redirect( - "/styles/test_style?version=1.0", - "/style/test_style?version=1.0", - CONFIG_WITH_STYLES, - ) - .await; -} - -#[actix_rt::test] -#[cfg(feature = "sprites")] -async fn test_redirect_sprites_preserves_query_string() { - assert_redirect( - "/sprites/src1.json?scale=2", - "/sprite/src1.json?scale=2", - CONFIG_WITH_SPRITES, - ) - .await; -} - -#[actix_rt::test] -#[cfg(feature = "mbtiles")] -async fn test_redirect_tile_preserves_query_string() { - let config = indoc! {" - mbtiles: - sources: - world_cities: ../tests/fixtures/mbtiles/world_cities.mbtiles - "}; - assert_redirect( - "/world_cities/0/0/0.pbf?format=json", - "/world_cities/0/0/0?format=json", - config, - ) - .await; -} From e317695626eecfe63ac4bc3456997b058b1a8f6a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 10:14:53 +0000 Subject: [PATCH 06/37] Change redirect handler visibility to pub(crate) Redirect handlers are only used internally within the server module and should not be part of the public API. Co-authored-by: CommanderStorm <26258709+CommanderStorm@users.noreply.github.com> --- martin/src/srv/fonts.rs | 2 +- martin/src/srv/sprites.rs | 8 ++++---- martin/src/srv/styles.rs | 2 +- martin/src/srv/tiles/content.rs | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/martin/src/srv/fonts.rs b/martin/src/srv/fonts.rs index c86f9b869..70fece717 100644 --- a/martin/src/srv/fonts.rs +++ b/martin/src/srv/fonts.rs @@ -46,7 +46,7 @@ async fn get_font( /// Redirect `/fonts/{fontstack}/{start}-{end}` to `/font/{fontstack}/{start}-{end}` (HTTP 301) #[route("/fonts/{fontstack}/{start}-{end}", method = "GET", method = "HEAD")] -pub async fn redirect_fonts(req: HttpRequest, path: Path) -> HttpResponse { +pub(crate) async fn redirect_fonts(req: HttpRequest, path: Path) -> HttpResponse { let location = format!("/font/{}/{}-{}", path.fontstack, path.start, path.end); let location = if req.query_string().is_empty() { location diff --git a/martin/src/srv/sprites.rs b/martin/src/srv/sprites.rs index c629be794..371f5264b 100644 --- a/martin/src/srv/sprites.rs +++ b/martin/src/srv/sprites.rs @@ -44,7 +44,7 @@ async fn get_sprite_png( /// Redirect `/sprites/{source_ids}.png` to `/sprite/{source_ids}.png` (HTTP 301) #[route("/sprites/{source_ids}.png", method = "GET", method = "HEAD")] -pub async fn redirect_sprites_png(req: HttpRequest, path: Path) -> HttpResponse { +pub(crate) async fn redirect_sprites_png(req: HttpRequest, path: Path) -> HttpResponse { redirect_with_query(&format!("/sprite/{}.png", path.source_ids), req.query_string()) } @@ -76,7 +76,7 @@ async fn get_sprite_sdf_png( /// Redirect `/sdf_sprites/{source_ids}.png` to `/sdf_sprite/{source_ids}.png` (HTTP 301) #[route("/sdf_sprites/{source_ids}.png", method = "GET", method = "HEAD")] -pub async fn redirect_sdf_sprites_png(req: HttpRequest, path: Path) -> HttpResponse { +pub(crate) async fn redirect_sdf_sprites_png(req: HttpRequest, path: Path) -> HttpResponse { redirect_with_query( &format!("/sdf_sprite/{}.png", path.source_ids), req.query_string(), @@ -112,7 +112,7 @@ async fn get_sprite_json( /// Redirect `/sprites/{source_ids}.json` to `/sprite/{source_ids}.json` (HTTP 301) #[route("/sprites/{source_ids}.json", method = "GET", method = "HEAD")] -pub async fn redirect_sprites_json(req: HttpRequest, path: Path) -> HttpResponse { +pub(crate) async fn redirect_sprites_json(req: HttpRequest, path: Path) -> HttpResponse { redirect_with_query( &format!("/sprite/{}.json", path.source_ids), req.query_string(), @@ -148,7 +148,7 @@ async fn get_sprite_sdf_json( /// Redirect `/sdf_sprites/{source_ids}.json` to `/sdf_sprite/{source_ids}.json` (HTTP 301) #[route("/sdf_sprites/{source_ids}.json", method = "GET", method = "HEAD")] -pub async fn redirect_sdf_sprites_json(req: HttpRequest, path: Path) -> HttpResponse { +pub(crate) async fn redirect_sdf_sprites_json(req: HttpRequest, path: Path) -> HttpResponse { redirect_with_query( &format!("/sdf_sprite/{}.json", path.source_ids), req.query_string(), diff --git a/martin/src/srv/styles.rs b/martin/src/srv/styles.rs index b33f15c37..4b0082650 100644 --- a/martin/src/srv/styles.rs +++ b/martin/src/srv/styles.rs @@ -52,7 +52,7 @@ async fn get_style_json(path: Path, styles: Data) -> /// Redirect `/styles/{style_id}` to `/style/{style_id}` (HTTP 301) /// This handles common pluralization mistakes #[route("/styles/{style_id}", method = "GET", method = "HEAD")] -pub async fn redirect_styles(req: HttpRequest, path: Path) -> HttpResponse { +pub(crate) async fn redirect_styles(req: HttpRequest, path: Path) -> HttpResponse { let location = format!("/style/{}", path.style_id); let location = if req.query_string().is_empty() { location diff --git a/martin/src/srv/tiles/content.rs b/martin/src/srv/tiles/content.rs index bf10e086e..ff8f43a14 100644 --- a/martin/src/srv/tiles/content.rs +++ b/martin/src/srv/tiles/content.rs @@ -64,27 +64,27 @@ async fn get_tile( /// Redirect `/{source_ids}/{z}/{x}/{y}.pbf` to `/{source_ids}/{z}/{x}/{y}` (HTTP 301) /// Registered before main tile route to match more specific pattern first #[route("/{source_ids}/{z}/{x}/{y}.pbf", method = "GET", method = "HEAD")] -pub async fn redirect_tile_pbf(req: HttpRequest, path: Path) -> HttpResponse { +pub(crate) async fn redirect_tile_pbf(req: HttpRequest, path: Path) -> HttpResponse { redirect_tile_with_query(&path.source_ids, path.z, path.x, path.y, req.query_string()) } /// Redirect `/{source_ids}/{z}/{x}/{y}.mvt` to `/{source_ids}/{z}/{x}/{y}` (HTTP 301) /// Registered before main tile route to match more specific pattern first #[route("/{source_ids}/{z}/{x}/{y}.mvt", method = "GET", method = "HEAD")] -pub async fn redirect_tile_mvt(req: HttpRequest, path: Path) -> HttpResponse { +pub(crate) async fn redirect_tile_mvt(req: HttpRequest, path: Path) -> HttpResponse { redirect_tile_with_query(&path.source_ids, path.z, path.x, path.y, req.query_string()) } /// Redirect `/{source_ids}/{z}/{x}/{y}.mlt` to `/{source_ids}/{z}/{x}/{y}` (HTTP 301) /// Registered before main tile route to match more specific pattern first #[route("/{source_ids}/{z}/{x}/{y}.mlt", method = "GET", method = "HEAD")] -pub async fn redirect_tile_mlt(req: HttpRequest, path: Path) -> HttpResponse { +pub(crate) async fn redirect_tile_mlt(req: HttpRequest, path: Path) -> HttpResponse { redirect_tile_with_query(&path.source_ids, path.z, path.x, path.y, req.query_string()) } /// Redirect `/tiles/{source_ids}/{z}/{x}/{y}` to `/{source_ids}/{z}/{x}/{y}` (HTTP 301) #[route("/tiles/{source_ids}/{z}/{x}/{y}", method = "GET", method = "HEAD")] -pub async fn redirect_tiles(req: HttpRequest, path: Path) -> HttpResponse { +pub(crate) async fn redirect_tiles(req: HttpRequest, path: Path) -> HttpResponse { redirect_tile_with_query(&path.source_ids, path.z, path.x, path.y, req.query_string()) } From be45c8a18080410b0be1045594b0faf0afce177f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 10:30:55 +0000 Subject: [PATCH 07/37] chore(fmt): apply pre-commit formatting fixes --- martin/src/srv/sprites.rs | 25 ++++++++++++++++++++----- martin/src/srv/tiles/content.rs | 4 ++-- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/martin/src/srv/sprites.rs b/martin/src/srv/sprites.rs index 371f5264b..0044ec3a6 100644 --- a/martin/src/srv/sprites.rs +++ b/martin/src/srv/sprites.rs @@ -44,8 +44,14 @@ async fn get_sprite_png( /// Redirect `/sprites/{source_ids}.png` to `/sprite/{source_ids}.png` (HTTP 301) #[route("/sprites/{source_ids}.png", method = "GET", method = "HEAD")] -pub(crate) async fn redirect_sprites_png(req: HttpRequest, path: Path) -> HttpResponse { - redirect_with_query(&format!("/sprite/{}.png", path.source_ids), req.query_string()) +pub(crate) async fn redirect_sprites_png( + req: HttpRequest, + path: Path, +) -> HttpResponse { + redirect_with_query( + &format!("/sprite/{}.png", path.source_ids), + req.query_string(), + ) } #[route( @@ -76,7 +82,10 @@ async fn get_sprite_sdf_png( /// Redirect `/sdf_sprites/{source_ids}.png` to `/sdf_sprite/{source_ids}.png` (HTTP 301) #[route("/sdf_sprites/{source_ids}.png", method = "GET", method = "HEAD")] -pub(crate) async fn redirect_sdf_sprites_png(req: HttpRequest, path: Path) -> HttpResponse { +pub(crate) async fn redirect_sdf_sprites_png( + req: HttpRequest, + path: Path, +) -> HttpResponse { redirect_with_query( &format!("/sdf_sprite/{}.png", path.source_ids), req.query_string(), @@ -112,7 +121,10 @@ async fn get_sprite_json( /// Redirect `/sprites/{source_ids}.json` to `/sprite/{source_ids}.json` (HTTP 301) #[route("/sprites/{source_ids}.json", method = "GET", method = "HEAD")] -pub(crate) async fn redirect_sprites_json(req: HttpRequest, path: Path) -> HttpResponse { +pub(crate) async fn redirect_sprites_json( + req: HttpRequest, + path: Path, +) -> HttpResponse { redirect_with_query( &format!("/sprite/{}.json", path.source_ids), req.query_string(), @@ -148,7 +160,10 @@ async fn get_sprite_sdf_json( /// Redirect `/sdf_sprites/{source_ids}.json` to `/sdf_sprite/{source_ids}.json` (HTTP 301) #[route("/sdf_sprites/{source_ids}.json", method = "GET", method = "HEAD")] -pub(crate) async fn redirect_sdf_sprites_json(req: HttpRequest, path: Path) -> HttpResponse { +pub(crate) async fn redirect_sdf_sprites_json( + req: HttpRequest, + path: Path, +) -> HttpResponse { redirect_with_query( &format!("/sdf_sprite/{}.json", path.source_ids), req.query_string(), diff --git a/martin/src/srv/tiles/content.rs b/martin/src/srv/tiles/content.rs index ff8f43a14..77103f83b 100644 --- a/martin/src/srv/tiles/content.rs +++ b/martin/src/srv/tiles/content.rs @@ -2,8 +2,8 @@ use actix_http::ContentEncoding; use actix_http::header::Quality; use actix_web::error::{ErrorBadRequest, ErrorNotAcceptable, ErrorNotFound}; use actix_web::http::header::{ - AcceptEncoding, CONTENT_ENCODING, ETAG, LOCATION, Encoding as HeaderEnc, EntityTag, - IfNoneMatch, Preference, + AcceptEncoding, CONTENT_ENCODING, ETAG, Encoding as HeaderEnc, EntityTag, IfNoneMatch, + LOCATION, Preference, }; use actix_web::web::{Data, Path, Query}; use actix_web::{HttpMessage, HttpRequest, HttpResponse, Result as ActixResult, route}; From ba0b9977865bff1659ea168e67c3bc127adaea69 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Tue, 27 Jan 2026 11:47:23 +0100 Subject: [PATCH 08/37] Apply suggestions from code review --- martin/src/srv/fonts.rs | 10 ++------ martin/src/srv/server.rs | 28 +++++++++-------------- martin/src/srv/sprites.rs | 48 +++++++++++++-------------------------- martin/src/srv/styles.rs | 10 ++------ 4 files changed, 31 insertions(+), 65 deletions(-) diff --git a/martin/src/srv/fonts.rs b/martin/src/srv/fonts.rs index 70fece717..7ea188f62 100644 --- a/martin/src/srv/fonts.rs +++ b/martin/src/srv/fonts.rs @@ -46,15 +46,9 @@ async fn get_font( /// Redirect `/fonts/{fontstack}/{start}-{end}` to `/font/{fontstack}/{start}-{end}` (HTTP 301) #[route("/fonts/{fontstack}/{start}-{end}", method = "GET", method = "HEAD")] -pub(crate) async fn redirect_fonts(req: HttpRequest, path: Path) -> HttpResponse { - let location = format!("/font/{}/{}-{}", path.fontstack, path.start, path.end); - let location = if req.query_string().is_empty() { - location - } else { - format!("{location}?{}", req.query_string()) - }; +pub async fn redirect_fonts(path: Path) -> HttpResponse { HttpResponse::MovedPermanently() - .insert_header((LOCATION, location)) + .insert_header((LOCATION, format!("/font/{}/{}-{}", path.fontstack, path.start, path.end))) .finish() } diff --git a/martin/src/srv/server.rs b/martin/src/srv/server.rs index 68de99606..4ebd6787e 100644 --- a/martin/src/srv/server.rs +++ b/martin/src/srv/server.rs @@ -73,31 +73,25 @@ fn register_services(cfg: &mut web::ServiceConfig, #[allow(unused_variables)] us #[cfg(feature = "sprites")] { cfg.service(crate::srv::sprites::get_sprite_sdf_json) - .service(crate::srv::sprites::get_sprite_json) - .service(crate::srv::sprites::get_sprite_sdf_png) - .service(crate::srv::sprites::get_sprite_png); - - // Register sprite plural redirects - cfg.service(crate::srv::sprites::redirect_sprites_json) - .service(crate::srv::sprites::redirect_sprites_png) - .service(crate::srv::sprites::redirect_sdf_sprites_json) - .service(crate::srv::sprites::redirect_sdf_sprites_png); + .service(crate::srv::sprites::redirect_sdf_sprites_json) + .service(crate::srv::sprites::get_sprite_json) + .service(crate::srv::sprites::redirect_sprites_json) + .service(crate::srv::sprites::get_sprite_sdf_png) + .service(crate::srv::sprites::redirect_sdf_sprites_png) + .service(crate::srv::sprites::get_sprite_png) + .service(crate::srv::sprites::redirect_sprites_png); } #[cfg(feature = "fonts")] { - cfg.service(crate::srv::fonts::get_font); - - // Register fonts plural redirect - cfg.service(crate::srv::fonts::redirect_fonts); + cfg.service(crate::srv::fonts::get_font) + .service(crate::srv::fonts::redirect_fonts); } #[cfg(feature = "styles")] { - cfg.service(crate::srv::styles::get_style_json); - - // Register styles plural redirect - cfg.service(crate::srv::styles::redirect_styles); + cfg.service(crate::srv::styles::get_style_json) + .service(crate::srv::styles::redirect_styles); } #[cfg(all(feature = "unstable-rendering", target_os = "linux"))] diff --git a/martin/src/srv/sprites.rs b/martin/src/srv/sprites.rs index 0044ec3a6..e9b622979 100644 --- a/martin/src/srv/sprites.rs +++ b/martin/src/srv/sprites.rs @@ -44,14 +44,10 @@ async fn get_sprite_png( /// Redirect `/sprites/{source_ids}.png` to `/sprite/{source_ids}.png` (HTTP 301) #[route("/sprites/{source_ids}.png", method = "GET", method = "HEAD")] -pub(crate) async fn redirect_sprites_png( - req: HttpRequest, - path: Path, -) -> HttpResponse { - redirect_with_query( - &format!("/sprite/{}.png", path.source_ids), - req.query_string(), - ) +pub async fn redirect_sprites_png(path: Path) -> HttpResponse { + HttpResponse::MovedPermanently() + .insert_header((LOCATION, format!("/sprite/{}.png", path.source_ids))) + .finish() } #[route( @@ -82,14 +78,10 @@ async fn get_sprite_sdf_png( /// Redirect `/sdf_sprites/{source_ids}.png` to `/sdf_sprite/{source_ids}.png` (HTTP 301) #[route("/sdf_sprites/{source_ids}.png", method = "GET", method = "HEAD")] -pub(crate) async fn redirect_sdf_sprites_png( - req: HttpRequest, - path: Path, -) -> HttpResponse { - redirect_with_query( - &format!("/sdf_sprite/{}.png", path.source_ids), - req.query_string(), - ) +pub async fn redirect_sdf_sprites_png(path: Path) -> HttpResponse { + HttpResponse::MovedPermanently() + .insert_header((LOCATION, format!("/sdf_sprite/{}.png", path.source_ids))) + .finish() } #[route( @@ -121,14 +113,10 @@ async fn get_sprite_json( /// Redirect `/sprites/{source_ids}.json` to `/sprite/{source_ids}.json` (HTTP 301) #[route("/sprites/{source_ids}.json", method = "GET", method = "HEAD")] -pub(crate) async fn redirect_sprites_json( - req: HttpRequest, - path: Path, -) -> HttpResponse { - redirect_with_query( - &format!("/sprite/{}.json", path.source_ids), - req.query_string(), - ) +pub async fn redirect_sprites_json(path: Path) -> HttpResponse { + HttpResponse::MovedPermanently() + .insert_header((LOCATION, format!("/sprite/{}.json", path.source_ids))) + .finish() } #[route( @@ -160,14 +148,10 @@ async fn get_sprite_sdf_json( /// Redirect `/sdf_sprites/{source_ids}.json` to `/sdf_sprite/{source_ids}.json` (HTTP 301) #[route("/sdf_sprites/{source_ids}.json", method = "GET", method = "HEAD")] -pub(crate) async fn redirect_sdf_sprites_json( - req: HttpRequest, - path: Path, -) -> HttpResponse { - redirect_with_query( - &format!("/sdf_sprite/{}.json", path.source_ids), - req.query_string(), - ) +pub(crate) async fn redirect_sdf_sprites_json(path: Path) -> HttpResponse { + HttpResponse::MovedPermanently() + .insert_header((LOCATION, format!("/sdf_sprite/{}.json", path.source_ids))) + .finish() } async fn get_sprite(source_ids: &str, sprites: &SpriteSources, as_sdf: bool) -> ActixResult { diff --git a/martin/src/srv/styles.rs b/martin/src/srv/styles.rs index 4b0082650..ccc93840b 100644 --- a/martin/src/srv/styles.rs +++ b/martin/src/srv/styles.rs @@ -52,14 +52,8 @@ async fn get_style_json(path: Path, styles: Data) -> /// Redirect `/styles/{style_id}` to `/style/{style_id}` (HTTP 301) /// This handles common pluralization mistakes #[route("/styles/{style_id}", method = "GET", method = "HEAD")] -pub(crate) async fn redirect_styles(req: HttpRequest, path: Path) -> HttpResponse { - let location = format!("/style/{}", path.style_id); - let location = if req.query_string().is_empty() { - location - } else { - format!("{location}?{}", req.query_string()) - }; +pub(crate) async fn redirect_styles(path: Path) -> HttpResponse { HttpResponse::MovedPermanently() - .insert_header((LOCATION, location)) + .insert_header((LOCATION, format!("/style/{}", path.style_id))) .finish() } From 22a272a3f689a74d0be9a1561e07e5f9c487624e Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Tue, 27 Jan 2026 11:47:38 +0100 Subject: [PATCH 09/37] Apply suggestion from @CommanderStorm --- martin/src/srv/sprites.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/martin/src/srv/sprites.rs b/martin/src/srv/sprites.rs index e9b622979..e1b6992ab 100644 --- a/martin/src/srv/sprites.rs +++ b/martin/src/srv/sprites.rs @@ -177,15 +177,3 @@ async fn get_index(source_ids: &str, sprites: &SpriteSources, as_sdf: bool) -> A let json = serde_json::to_vec(&sheet.get_index()).map_err(map_internal_error)?; Ok(Bytes::from(json)) } - -/// Helper function to create a 301 redirect with query string preservation -fn redirect_with_query(target_path: &str, query_string: &str) -> HttpResponse { - let location = if query_string.is_empty() { - target_path.to_string() - } else { - format!("{target_path}?{query_string}") - }; - HttpResponse::MovedPermanently() - .insert_header((LOCATION, location)) - .finish() -} From 69aabd97b5c73d36289b9348ae20b6b440e372ec Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 10:47:42 +0000 Subject: [PATCH 10/37] chore(fmt): apply pre-commit formatting fixes --- martin/src/srv/fonts.rs | 5 ++++- martin/src/srv/server.rs | 18 +++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/martin/src/srv/fonts.rs b/martin/src/srv/fonts.rs index 7ea188f62..a3d31f95d 100644 --- a/martin/src/srv/fonts.rs +++ b/martin/src/srv/fonts.rs @@ -48,7 +48,10 @@ async fn get_font( #[route("/fonts/{fontstack}/{start}-{end}", method = "GET", method = "HEAD")] pub async fn redirect_fonts(path: Path) -> HttpResponse { HttpResponse::MovedPermanently() - .insert_header((LOCATION, format!("/font/{}/{}-{}", path.fontstack, path.start, path.end))) + .insert_header(( + LOCATION, + format!("/font/{}/{}-{}", path.fontstack, path.start, path.end), + )) .finish() } diff --git a/martin/src/srv/server.rs b/martin/src/srv/server.rs index 4ebd6787e..8a93461f8 100644 --- a/martin/src/srv/server.rs +++ b/martin/src/srv/server.rs @@ -73,25 +73,25 @@ fn register_services(cfg: &mut web::ServiceConfig, #[allow(unused_variables)] us #[cfg(feature = "sprites")] { cfg.service(crate::srv::sprites::get_sprite_sdf_json) - .service(crate::srv::sprites::redirect_sdf_sprites_json) - .service(crate::srv::sprites::get_sprite_json) - .service(crate::srv::sprites::redirect_sprites_json) - .service(crate::srv::sprites::get_sprite_sdf_png) - .service(crate::srv::sprites::redirect_sdf_sprites_png) - .service(crate::srv::sprites::get_sprite_png) - .service(crate::srv::sprites::redirect_sprites_png); + .service(crate::srv::sprites::redirect_sdf_sprites_json) + .service(crate::srv::sprites::get_sprite_json) + .service(crate::srv::sprites::redirect_sprites_json) + .service(crate::srv::sprites::get_sprite_sdf_png) + .service(crate::srv::sprites::redirect_sdf_sprites_png) + .service(crate::srv::sprites::get_sprite_png) + .service(crate::srv::sprites::redirect_sprites_png); } #[cfg(feature = "fonts")] { cfg.service(crate::srv::fonts::get_font) - .service(crate::srv::fonts::redirect_fonts); + .service(crate::srv::fonts::redirect_fonts); } #[cfg(feature = "styles")] { cfg.service(crate::srv::styles::get_style_json) - .service(crate::srv::styles::redirect_styles); + .service(crate::srv::styles::redirect_styles); } #[cfg(all(feature = "unstable-rendering", target_os = "linux"))] From 71062a893979cd824cdb630efcee9e8b87fee408 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Tue, 27 Jan 2026 11:49:00 +0100 Subject: [PATCH 11/37] Apply suggestions from code review --- martin/src/srv/sprites.rs | 2 +- martin/src/srv/tiles/content.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/martin/src/srv/sprites.rs b/martin/src/srv/sprites.rs index e1b6992ab..dca2fa1d8 100644 --- a/martin/src/srv/sprites.rs +++ b/martin/src/srv/sprites.rs @@ -148,7 +148,7 @@ async fn get_sprite_sdf_json( /// Redirect `/sdf_sprites/{source_ids}.json` to `/sdf_sprite/{source_ids}.json` (HTTP 301) #[route("/sdf_sprites/{source_ids}.json", method = "GET", method = "HEAD")] -pub(crate) async fn redirect_sdf_sprites_json(path: Path) -> HttpResponse { +pub async fn redirect_sdf_sprites_json(path: Path) -> HttpResponse { HttpResponse::MovedPermanently() .insert_header((LOCATION, format!("/sdf_sprite/{}.json", path.source_ids))) .finish() diff --git a/martin/src/srv/tiles/content.rs b/martin/src/srv/tiles/content.rs index 77103f83b..670857c0c 100644 --- a/martin/src/srv/tiles/content.rs +++ b/martin/src/srv/tiles/content.rs @@ -64,27 +64,27 @@ async fn get_tile( /// Redirect `/{source_ids}/{z}/{x}/{y}.pbf` to `/{source_ids}/{z}/{x}/{y}` (HTTP 301) /// Registered before main tile route to match more specific pattern first #[route("/{source_ids}/{z}/{x}/{y}.pbf", method = "GET", method = "HEAD")] -pub(crate) async fn redirect_tile_pbf(req: HttpRequest, path: Path) -> HttpResponse { +pub async fn redirect_tile_pbf(req: HttpRequest, path: Path) -> HttpResponse { redirect_tile_with_query(&path.source_ids, path.z, path.x, path.y, req.query_string()) } /// Redirect `/{source_ids}/{z}/{x}/{y}.mvt` to `/{source_ids}/{z}/{x}/{y}` (HTTP 301) /// Registered before main tile route to match more specific pattern first #[route("/{source_ids}/{z}/{x}/{y}.mvt", method = "GET", method = "HEAD")] -pub(crate) async fn redirect_tile_mvt(req: HttpRequest, path: Path) -> HttpResponse { +pub async fn redirect_tile_mvt(req: HttpRequest, path: Path) -> HttpResponse { redirect_tile_with_query(&path.source_ids, path.z, path.x, path.y, req.query_string()) } /// Redirect `/{source_ids}/{z}/{x}/{y}.mlt` to `/{source_ids}/{z}/{x}/{y}` (HTTP 301) /// Registered before main tile route to match more specific pattern first #[route("/{source_ids}/{z}/{x}/{y}.mlt", method = "GET", method = "HEAD")] -pub(crate) async fn redirect_tile_mlt(req: HttpRequest, path: Path) -> HttpResponse { +pub async fn redirect_tile_mlt(req: HttpRequest, path: Path) -> HttpResponse { redirect_tile_with_query(&path.source_ids, path.z, path.x, path.y, req.query_string()) } /// Redirect `/tiles/{source_ids}/{z}/{x}/{y}` to `/{source_ids}/{z}/{x}/{y}` (HTTP 301) #[route("/tiles/{source_ids}/{z}/{x}/{y}", method = "GET", method = "HEAD")] -pub(crate) async fn redirect_tiles(req: HttpRequest, path: Path) -> HttpResponse { +pub async fn redirect_tiles(req: HttpRequest, path: Path) -> HttpResponse { redirect_tile_with_query(&path.source_ids, path.z, path.x, path.y, req.query_string()) } From edffef73e27087310e7621e63cfff4010334d082 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Tue, 27 Jan 2026 11:50:05 +0100 Subject: [PATCH 12/37] Apply suggestions from code review --- martin/src/srv/sprites.rs | 2 +- martin/src/srv/styles.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/martin/src/srv/sprites.rs b/martin/src/srv/sprites.rs index dca2fa1d8..b2d58e0e6 100644 --- a/martin/src/srv/sprites.rs +++ b/martin/src/srv/sprites.rs @@ -5,7 +5,7 @@ use actix_web::error::ErrorNotFound; use actix_web::http::header::{ContentType, LOCATION}; use actix_web::middleware::Compress; use actix_web::web::{Bytes, Data, Path}; -use actix_web::{HttpRequest, HttpResponse, Result as ActixResult, route}; +use actix_web::{HttpResponse, Result as ActixResult, route}; use martin_core::sprites::{OptSpriteCache, SpriteError, SpriteSources}; use serde::Deserialize; diff --git a/martin/src/srv/styles.rs b/martin/src/srv/styles.rs index ccc93840b..bd3d6c0a1 100644 --- a/martin/src/srv/styles.rs +++ b/martin/src/srv/styles.rs @@ -2,7 +2,7 @@ use actix_middleware_etag::Etag; use actix_web::http::header::{ContentType, LOCATION}; use actix_web::middleware::Compress; use actix_web::web::{Data, Path}; -use actix_web::{HttpRequest, HttpResponse, route}; +use actix_web::{HttpResponse, route}; use martin_core::styles::StyleSources; use serde::Deserialize; use tracing::error; From f87ccf983101ebcf231efd538c97fe45648300e6 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Tue, 27 Jan 2026 11:51:09 +0100 Subject: [PATCH 13/37] Apply suggestions from code review --- martin/src/srv/server.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/martin/src/srv/server.rs b/martin/src/srv/server.rs index 8a93461f8..93b30e608 100644 --- a/martin/src/srv/server.rs +++ b/martin/src/srv/server.rs @@ -71,7 +71,6 @@ fn register_services(cfg: &mut web::ServiceConfig, #[allow(unused_variables)] us } #[cfg(feature = "sprites")] - { cfg.service(crate::srv::sprites::get_sprite_sdf_json) .service(crate::srv::sprites::redirect_sdf_sprites_json) .service(crate::srv::sprites::get_sprite_json) @@ -80,19 +79,14 @@ fn register_services(cfg: &mut web::ServiceConfig, #[allow(unused_variables)] us .service(crate::srv::sprites::redirect_sdf_sprites_png) .service(crate::srv::sprites::get_sprite_png) .service(crate::srv::sprites::redirect_sprites_png); - } #[cfg(feature = "fonts")] - { cfg.service(crate::srv::fonts::get_font) .service(crate::srv::fonts::redirect_fonts); - } #[cfg(feature = "styles")] - { cfg.service(crate::srv::styles::get_style_json) .service(crate::srv::styles::redirect_styles); - } #[cfg(all(feature = "unstable-rendering", target_os = "linux"))] cfg.service(crate::srv::styles_rendering::get_style_rendered); From d0236ccb17f66a70f7c6ce0f0ff9d1858bf9175d Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Tue, 27 Jan 2026 11:51:39 +0100 Subject: [PATCH 14/37] Apply suggestion from @CommanderStorm --- martin/src/srv/fonts.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/martin/src/srv/fonts.rs b/martin/src/srv/fonts.rs index a3d31f95d..130ae7f5d 100644 --- a/martin/src/srv/fonts.rs +++ b/martin/src/srv/fonts.rs @@ -5,7 +5,7 @@ use actix_web::error::{ErrorBadRequest, ErrorNotFound}; use actix_web::http::header::LOCATION; use actix_web::middleware::Compress; use actix_web::web::{Data, Path}; -use actix_web::{HttpRequest, HttpResponse, Result as ActixResult, route}; +use actix_web::{HttpResponse, Result as ActixResult, route}; use martin_core::fonts::{FontError, FontSources, OptFontCache}; use serde::Deserialize; From ef8d7c4e00c95dea89f6b2833483e9c0e7a155e0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 10:53:29 +0000 Subject: [PATCH 15/37] chore(fmt): apply pre-commit formatting fixes --- martin/src/srv/server.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/martin/src/srv/server.rs b/martin/src/srv/server.rs index 93b30e608..2981913ec 100644 --- a/martin/src/srv/server.rs +++ b/martin/src/srv/server.rs @@ -71,22 +71,22 @@ fn register_services(cfg: &mut web::ServiceConfig, #[allow(unused_variables)] us } #[cfg(feature = "sprites")] - cfg.service(crate::srv::sprites::get_sprite_sdf_json) - .service(crate::srv::sprites::redirect_sdf_sprites_json) - .service(crate::srv::sprites::get_sprite_json) - .service(crate::srv::sprites::redirect_sprites_json) - .service(crate::srv::sprites::get_sprite_sdf_png) - .service(crate::srv::sprites::redirect_sdf_sprites_png) - .service(crate::srv::sprites::get_sprite_png) - .service(crate::srv::sprites::redirect_sprites_png); + cfg.service(crate::srv::sprites::get_sprite_sdf_json) + .service(crate::srv::sprites::redirect_sdf_sprites_json) + .service(crate::srv::sprites::get_sprite_json) + .service(crate::srv::sprites::redirect_sprites_json) + .service(crate::srv::sprites::get_sprite_sdf_png) + .service(crate::srv::sprites::redirect_sdf_sprites_png) + .service(crate::srv::sprites::get_sprite_png) + .service(crate::srv::sprites::redirect_sprites_png); #[cfg(feature = "fonts")] - cfg.service(crate::srv::fonts::get_font) - .service(crate::srv::fonts::redirect_fonts); + cfg.service(crate::srv::fonts::get_font) + .service(crate::srv::fonts::redirect_fonts); #[cfg(feature = "styles")] - cfg.service(crate::srv::styles::get_style_json) - .service(crate::srv::styles::redirect_styles); + cfg.service(crate::srv::styles::get_style_json) + .service(crate::srv::styles::redirect_styles); #[cfg(all(feature = "unstable-rendering", target_os = "linux"))] cfg.service(crate::srv::styles_rendering::get_style_rendered); From 7438f1a4721143b7d3eb035ecff78839bfee287b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:02:47 +0000 Subject: [PATCH 16/37] Add integration tests for URL redirects in test.sh - Added test_redirect function to verify HTTP 301 redirects - Tests for pluralization redirects (styles, sprites, fonts) - Tests for tile format suffix redirects (.pbf, .mvt, .mlt) - Tests for /tiles/ prefix redirect - Tests for query string preservation - All tests validate both HTTP status code and Location header Co-authored-by: CommanderStorm <26258709+CommanderStorm@users.noreply.github.com> --- tests/test.sh | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/test.sh b/tests/test.sh index b32527ac2..bbd532359 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -182,6 +182,30 @@ test_font() { clean_headers_dump "$FILENAME.headers" } +test_redirect() { + FILENAME="$TEST_OUT_DIR/$1.redirect" + URL="$MARTIN_URL/$2" + EXPECTED_LOCATION="$3" + + echo "Testing redirect from $URL to $EXPECTED_LOCATION" + # Use curl without --fail to allow 3xx responses, and follow redirects with -L + HTTP_CODE=$(curl --silent --show-error --write-out "%{http_code}" --output /dev/null --head "$URL") + LOCATION=$(curl --silent --show-error --head "$URL" | grep -i "^location:" | $SED 's/^[Ll]ocation: *//' | tr -d '\r') + + echo "$HTTP_CODE" > "$FILENAME.code" + echo "$LOCATION" > "$FILENAME.location" + + if [ "$HTTP_CODE" != "301" ]; then + echo "ERROR: Expected HTTP 301, got $HTTP_CODE for $URL" + exit 1 + fi + + if [ "$LOCATION" != "$EXPECTED_LOCATION" ]; then + echo "ERROR: Expected location '$EXPECTED_LOCATION', got '$LOCATION' for $URL" + exit 1 + fi +} + # Delete line from a file $1 that matches parameter $2 and log the action remove_lines() { FILE="$1" @@ -563,6 +587,28 @@ test_font font_1 font/Overpass%20Mono%20Light/0-255 test_font font_2 font/Overpass%20Mono%20Regular/0-255 test_font font_3 font/Overpass%20Mono%20Regular,Overpass%20Mono%20Light/0-255 +>&2 echo "***** Test URL redirects (HTTP 301) *****" + +# Test pluralization redirects +test_redirect redirect_styles styles/maplibre /style/maplibre +test_redirect redirect_sprites_json sprites/src1.json /sprite/src1.json +test_redirect redirect_sprites_png sprites/src1.png /sprite/src1.png +test_redirect redirect_sdf_sprites_json sdf_sprites/src1.json /sdf_sprite/src1.json +test_redirect redirect_sdf_sprites_png sdf_sprites/src1.png /sdf_sprite/src1.png +test_redirect redirect_fonts "fonts/Overpass%20Mono%20Regular/0-255" "/font/Overpass%20Mono%20Regular/0-255" + +# Test tile format suffix redirects +test_redirect redirect_tile_pbf stamen_toner__raster_CC-BY-ODbL_z3/0/0/0.pbf /stamen_toner__raster_CC-BY-ODbL_z3/0/0/0 +test_redirect redirect_tile_mvt stamen_toner__raster_CC-BY-ODbL_z3/0/0/0.mvt /stamen_toner__raster_CC-BY-ODbL_z3/0/0/0 +test_redirect redirect_tile_mlt stamen_toner__raster_CC-BY-ODbL_z3/0/0/0.mlt /stamen_toner__raster_CC-BY-ODbL_z3/0/0/0 + +# Test /tiles/ prefix redirect +test_redirect redirect_tiles_prefix tiles/stamen_toner__raster_CC-BY-ODbL_z3/0/0/0 /stamen_toner__raster_CC-BY-ODbL_z3/0/0/0 + +# Test query string preservation +test_redirect redirect_query_string "styles/maplibre?version=1.0" "/style/maplibre?version=1.0" +test_redirect redirect_tile_query_string "stamen_toner__raster_CC-BY-ODbL_z3/0/0/0.pbf?test=123" "/stamen_toner__raster_CC-BY-ODbL_z3/0/0/0?test=123" + # Test comments override test_jsn tbl_comment_cfg MixPoints test_jsn fnc_comment_cfg function_Mixed_Name From 3714de70d0d7b2a4a7ae1fa95a4b013dc5b6a9d9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:10:02 +0000 Subject: [PATCH 17/37] chore(fmt): apply pre-commit formatting fixes --- tests/test.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test.sh b/tests/test.sh index bbd532359..a31f570a6 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -191,15 +191,15 @@ test_redirect() { # Use curl without --fail to allow 3xx responses, and follow redirects with -L HTTP_CODE=$(curl --silent --show-error --write-out "%{http_code}" --output /dev/null --head "$URL") LOCATION=$(curl --silent --show-error --head "$URL" | grep -i "^location:" | $SED 's/^[Ll]ocation: *//' | tr -d '\r') - + echo "$HTTP_CODE" > "$FILENAME.code" echo "$LOCATION" > "$FILENAME.location" - + if [ "$HTTP_CODE" != "301" ]; then echo "ERROR: Expected HTTP 301, got $HTTP_CODE for $URL" exit 1 fi - + if [ "$LOCATION" != "$EXPECTED_LOCATION" ]; then echo "ERROR: Expected location '$EXPECTED_LOCATION', got '$LOCATION' for $URL" exit 1 From 4823da5f8f9a4e3e0fe9301fc0d4805d084f0db1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:48:11 +0000 Subject: [PATCH 18/37] Fix test_redirect function to not create litter files - Removed creation of .code and .location files - Simplified function signature (removed unused FILENAME parameter) - Removed incorrect query string test for styles - Query string preservation only tested for tiles (as intended) Co-authored-by: CommanderStorm <26258709+CommanderStorm@users.noreply.github.com> --- tests/test.sh | 39 +++++++++++++++++---------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/tests/test.sh b/tests/test.sh index a31f570a6..e708676e0 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -183,23 +183,19 @@ test_font() { } test_redirect() { - FILENAME="$TEST_OUT_DIR/$1.redirect" - URL="$MARTIN_URL/$2" - EXPECTED_LOCATION="$3" + URL="$MARTIN_URL/$1" + EXPECTED_LOCATION="$2" echo "Testing redirect from $URL to $EXPECTED_LOCATION" - # Use curl without --fail to allow 3xx responses, and follow redirects with -L + # Use curl without --fail to allow 3xx responses HTTP_CODE=$(curl --silent --show-error --write-out "%{http_code}" --output /dev/null --head "$URL") LOCATION=$(curl --silent --show-error --head "$URL" | grep -i "^location:" | $SED 's/^[Ll]ocation: *//' | tr -d '\r') - - echo "$HTTP_CODE" > "$FILENAME.code" - echo "$LOCATION" > "$FILENAME.location" - + if [ "$HTTP_CODE" != "301" ]; then echo "ERROR: Expected HTTP 301, got $HTTP_CODE for $URL" exit 1 fi - + if [ "$LOCATION" != "$EXPECTED_LOCATION" ]; then echo "ERROR: Expected location '$EXPECTED_LOCATION', got '$LOCATION' for $URL" exit 1 @@ -590,24 +586,23 @@ test_font font_3 font/Overpass%20Mono%20Regular,Overpass%20Mono%20Light/0-2 >&2 echo "***** Test URL redirects (HTTP 301) *****" # Test pluralization redirects -test_redirect redirect_styles styles/maplibre /style/maplibre -test_redirect redirect_sprites_json sprites/src1.json /sprite/src1.json -test_redirect redirect_sprites_png sprites/src1.png /sprite/src1.png -test_redirect redirect_sdf_sprites_json sdf_sprites/src1.json /sdf_sprite/src1.json -test_redirect redirect_sdf_sprites_png sdf_sprites/src1.png /sdf_sprite/src1.png -test_redirect redirect_fonts "fonts/Overpass%20Mono%20Regular/0-255" "/font/Overpass%20Mono%20Regular/0-255" +test_redirect styles/maplibre /style/maplibre +test_redirect sprites/src1.json /sprite/src1.json +test_redirect sprites/src1.png /sprite/src1.png +test_redirect sdf_sprites/src1.json /sdf_sprite/src1.json +test_redirect sdf_sprites/src1.png /sdf_sprite/src1.png +test_redirect "fonts/Overpass%20Mono%20Regular/0-255" "/font/Overpass%20Mono%20Regular/0-255" # Test tile format suffix redirects -test_redirect redirect_tile_pbf stamen_toner__raster_CC-BY-ODbL_z3/0/0/0.pbf /stamen_toner__raster_CC-BY-ODbL_z3/0/0/0 -test_redirect redirect_tile_mvt stamen_toner__raster_CC-BY-ODbL_z3/0/0/0.mvt /stamen_toner__raster_CC-BY-ODbL_z3/0/0/0 -test_redirect redirect_tile_mlt stamen_toner__raster_CC-BY-ODbL_z3/0/0/0.mlt /stamen_toner__raster_CC-BY-ODbL_z3/0/0/0 +test_redirect stamen_toner__raster_CC-BY-ODbL_z3/0/0/0.pbf /stamen_toner__raster_CC-BY-ODbL_z3/0/0/0 +test_redirect stamen_toner__raster_CC-BY-ODbL_z3/0/0/0.mvt /stamen_toner__raster_CC-BY-ODbL_z3/0/0/0 +test_redirect stamen_toner__raster_CC-BY-ODbL_z3/0/0/0.mlt /stamen_toner__raster_CC-BY-ODbL_z3/0/0/0 # Test /tiles/ prefix redirect -test_redirect redirect_tiles_prefix tiles/stamen_toner__raster_CC-BY-ODbL_z3/0/0/0 /stamen_toner__raster_CC-BY-ODbL_z3/0/0/0 +test_redirect tiles/stamen_toner__raster_CC-BY-ODbL_z3/0/0/0 /stamen_toner__raster_CC-BY-ODbL_z3/0/0/0 -# Test query string preservation -test_redirect redirect_query_string "styles/maplibre?version=1.0" "/style/maplibre?version=1.0" -test_redirect redirect_tile_query_string "stamen_toner__raster_CC-BY-ODbL_z3/0/0/0.pbf?test=123" "/stamen_toner__raster_CC-BY-ODbL_z3/0/0/0?test=123" +# Test query string preservation for tiles +test_redirect "stamen_toner__raster_CC-BY-ODbL_z3/0/0/0.pbf?test=123" "/stamen_toner__raster_CC-BY-ODbL_z3/0/0/0?test=123" # Test comments override test_jsn tbl_comment_cfg MixPoints From d8efebc2bbe2499871f91dff1eebf07b394fff02 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 12:02:44 +0000 Subject: [PATCH 19/37] chore(fmt): apply pre-commit formatting fixes --- tests/test.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test.sh b/tests/test.sh index e708676e0..051ace66f 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -190,12 +190,12 @@ test_redirect() { # Use curl without --fail to allow 3xx responses HTTP_CODE=$(curl --silent --show-error --write-out "%{http_code}" --output /dev/null --head "$URL") LOCATION=$(curl --silent --show-error --head "$URL" | grep -i "^location:" | $SED 's/^[Ll]ocation: *//' | tr -d '\r') - + if [ "$HTTP_CODE" != "301" ]; then echo "ERROR: Expected HTTP 301, got $HTTP_CODE for $URL" exit 1 fi - + if [ "$LOCATION" != "$EXPECTED_LOCATION" ]; then echo "ERROR: Expected location '$EXPECTED_LOCATION', got '$LOCATION' for $URL" exit 1 From 17834d27e7507241518cb47e331443bd0d6f0124 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Tue, 27 Jan 2026 20:29:35 +0100 Subject: [PATCH 20/37] simplify testcase --- .../metrics_1.fetched_with_compression.txt | 150 ++++++++++++++++++ tests/expected/configured/metrics_1.txt | 150 ++++++++++++++++++ tests/test.sh | 22 +-- 3 files changed, 311 insertions(+), 11 deletions(-) diff --git a/tests/expected/configured/metrics_1.fetched_with_compression.txt b/tests/expected/configured/metrics_1.fetched_with_compression.txt index ccc007fd9..56a6e4b43 100644 --- a/tests/expected/configured/metrics_1.fetched_with_compression.txt +++ b/tests/expected/configured/metrics_1.fetched_with_compression.txt @@ -42,6 +42,20 @@ martin_http_requests_duration_seconds_bucket{endpoint="/font/{fontstack}/{start} martin_http_requests_duration_seconds_bucket{endpoint="/font/{fontstack}/{start}-{end}",method="GET",status="200",le="+Inf"} NUMBER martin_http_requests_duration_seconds_sum{endpoint="/font/{fontstack}/{start}-{end}",method="GET",status="200"} NUMBER martin_http_requests_duration_seconds_count{endpoint="/font/{fontstack}/{start}-{end}",method="GET",status="200"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/fonts/{fontstack}/{start}-{end}",method="HEAD",status="301",le="0.005"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/fonts/{fontstack}/{start}-{end}",method="HEAD",status="301",le="0.01"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/fonts/{fontstack}/{start}-{end}",method="HEAD",status="301",le="0.025"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/fonts/{fontstack}/{start}-{end}",method="HEAD",status="301",le="0.05"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/fonts/{fontstack}/{start}-{end}",method="HEAD",status="301",le="0.1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/fonts/{fontstack}/{start}-{end}",method="HEAD",status="301",le="0.25"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/fonts/{fontstack}/{start}-{end}",method="HEAD",status="301",le="0.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/fonts/{fontstack}/{start}-{end}",method="HEAD",status="301",le="1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/fonts/{fontstack}/{start}-{end}",method="HEAD",status="301",le="2.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/fonts/{fontstack}/{start}-{end}",method="HEAD",status="301",le="5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/fonts/{fontstack}/{start}-{end}",method="HEAD",status="301",le="10"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/fonts/{fontstack}/{start}-{end}",method="HEAD",status="301",le="+Inf"} NUMBER +martin_http_requests_duration_seconds_sum{endpoint="/fonts/{fontstack}/{start}-{end}",method="HEAD",status="301"} NUMBER +martin_http_requests_duration_seconds_count{endpoint="/fonts/{fontstack}/{start}-{end}",method="HEAD",status="301"} NUMBER martin_http_requests_duration_seconds_bucket{endpoint="/health",method="GET",status="200",le="0.005"} NUMBER martin_http_requests_duration_seconds_bucket{endpoint="/health",method="GET",status="200",le="0.01"} NUMBER martin_http_requests_duration_seconds_bucket{endpoint="/health",method="GET",status="200",le="0.025"} NUMBER @@ -84,6 +98,34 @@ martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprite/{source_ids}. martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprite/{source_ids}.png",method="GET",status="200",le="+Inf"} NUMBER martin_http_requests_duration_seconds_sum{endpoint="/sdf_sprite/{source_ids}.png",method="GET",status="200"} NUMBER martin_http_requests_duration_seconds_count{endpoint="/sdf_sprite/{source_ids}.png",method="GET",status="200"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.json",method="HEAD",status="301",le="0.005"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.json",method="HEAD",status="301",le="0.01"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.json",method="HEAD",status="301",le="0.025"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.json",method="HEAD",status="301",le="0.05"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.json",method="HEAD",status="301",le="0.1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.json",method="HEAD",status="301",le="0.25"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.json",method="HEAD",status="301",le="0.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.json",method="HEAD",status="301",le="1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.json",method="HEAD",status="301",le="2.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.json",method="HEAD",status="301",le="5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.json",method="HEAD",status="301",le="10"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.json",method="HEAD",status="301",le="+Inf"} NUMBER +martin_http_requests_duration_seconds_sum{endpoint="/sdf_sprites/{source_ids}.json",method="HEAD",status="301"} NUMBER +martin_http_requests_duration_seconds_count{endpoint="/sdf_sprites/{source_ids}.json",method="HEAD",status="301"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.png",method="HEAD",status="301",le="0.005"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.png",method="HEAD",status="301",le="0.01"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.png",method="HEAD",status="301",le="0.025"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.png",method="HEAD",status="301",le="0.05"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.png",method="HEAD",status="301",le="0.1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.png",method="HEAD",status="301",le="0.25"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.png",method="HEAD",status="301",le="0.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.png",method="HEAD",status="301",le="1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.png",method="HEAD",status="301",le="2.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.png",method="HEAD",status="301",le="5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.png",method="HEAD",status="301",le="10"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.png",method="HEAD",status="301",le="+Inf"} NUMBER +martin_http_requests_duration_seconds_sum{endpoint="/sdf_sprites/{source_ids}.png",method="HEAD",status="301"} NUMBER +martin_http_requests_duration_seconds_count{endpoint="/sdf_sprites/{source_ids}.png",method="HEAD",status="301"} NUMBER martin_http_requests_duration_seconds_bucket{endpoint="/sprite/{source_ids}.json",method="GET",status="200",le="0.005"} NUMBER martin_http_requests_duration_seconds_bucket{endpoint="/sprite/{source_ids}.json",method="GET",status="200",le="0.01"} NUMBER martin_http_requests_duration_seconds_bucket{endpoint="/sprite/{source_ids}.json",method="GET",status="200",le="0.025"} NUMBER @@ -112,6 +154,34 @@ martin_http_requests_duration_seconds_bucket{endpoint="/sprite/{source_ids}.png" martin_http_requests_duration_seconds_bucket{endpoint="/sprite/{source_ids}.png",method="GET",status="200",le="+Inf"} NUMBER martin_http_requests_duration_seconds_sum{endpoint="/sprite/{source_ids}.png",method="GET",status="200"} NUMBER martin_http_requests_duration_seconds_count{endpoint="/sprite/{source_ids}.png",method="GET",status="200"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.json",method="HEAD",status="301",le="0.005"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.json",method="HEAD",status="301",le="0.01"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.json",method="HEAD",status="301",le="0.025"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.json",method="HEAD",status="301",le="0.05"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.json",method="HEAD",status="301",le="0.1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.json",method="HEAD",status="301",le="0.25"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.json",method="HEAD",status="301",le="0.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.json",method="HEAD",status="301",le="1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.json",method="HEAD",status="301",le="2.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.json",method="HEAD",status="301",le="5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.json",method="HEAD",status="301",le="10"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.json",method="HEAD",status="301",le="+Inf"} NUMBER +martin_http_requests_duration_seconds_sum{endpoint="/sprites/{source_ids}.json",method="HEAD",status="301"} NUMBER +martin_http_requests_duration_seconds_count{endpoint="/sprites/{source_ids}.json",method="HEAD",status="301"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.png",method="HEAD",status="301",le="0.005"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.png",method="HEAD",status="301",le="0.01"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.png",method="HEAD",status="301",le="0.025"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.png",method="HEAD",status="301",le="0.05"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.png",method="HEAD",status="301",le="0.1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.png",method="HEAD",status="301",le="0.25"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.png",method="HEAD",status="301",le="0.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.png",method="HEAD",status="301",le="1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.png",method="HEAD",status="301",le="2.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.png",method="HEAD",status="301",le="5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.png",method="HEAD",status="301",le="10"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.png",method="HEAD",status="301",le="+Inf"} NUMBER +martin_http_requests_duration_seconds_sum{endpoint="/sprites/{source_ids}.png",method="HEAD",status="301"} NUMBER +martin_http_requests_duration_seconds_count{endpoint="/sprites/{source_ids}.png",method="HEAD",status="301"} NUMBER martin_http_requests_duration_seconds_bucket{endpoint="/style/{style_id}",method="GET",status="200",le="0.005"} NUMBER martin_http_requests_duration_seconds_bucket{endpoint="/style/{style_id}",method="GET",status="200",le="0.01"} NUMBER martin_http_requests_duration_seconds_bucket{endpoint="/style/{style_id}",method="GET",status="200",le="0.025"} NUMBER @@ -126,6 +196,34 @@ martin_http_requests_duration_seconds_bucket{endpoint="/style/{style_id}",method martin_http_requests_duration_seconds_bucket{endpoint="/style/{style_id}",method="GET",status="200",le="+Inf"} NUMBER martin_http_requests_duration_seconds_sum{endpoint="/style/{style_id}",method="GET",status="200"} NUMBER martin_http_requests_duration_seconds_count{endpoint="/style/{style_id}",method="GET",status="200"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/styles/{style_id}",method="HEAD",status="301",le="0.005"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/styles/{style_id}",method="HEAD",status="301",le="0.01"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/styles/{style_id}",method="HEAD",status="301",le="0.025"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/styles/{style_id}",method="HEAD",status="301",le="0.05"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/styles/{style_id}",method="HEAD",status="301",le="0.1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/styles/{style_id}",method="HEAD",status="301",le="0.25"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/styles/{style_id}",method="HEAD",status="301",le="0.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/styles/{style_id}",method="HEAD",status="301",le="1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/styles/{style_id}",method="HEAD",status="301",le="2.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/styles/{style_id}",method="HEAD",status="301",le="5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/styles/{style_id}",method="HEAD",status="301",le="10"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/styles/{style_id}",method="HEAD",status="301",le="+Inf"} NUMBER +martin_http_requests_duration_seconds_sum{endpoint="/styles/{style_id}",method="HEAD",status="301"} NUMBER +martin_http_requests_duration_seconds_count{endpoint="/styles/{style_id}",method="HEAD",status="301"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301",le="0.005"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301",le="0.01"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301",le="0.025"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301",le="0.05"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301",le="0.1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301",le="0.25"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301",le="0.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301",le="1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301",le="2.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301",le="5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301",le="10"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301",le="+Inf"} NUMBER +martin_http_requests_duration_seconds_sum{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301"} NUMBER +martin_http_requests_duration_seconds_count{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301"} NUMBER martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}",method="GET",status="200",le="0.005"} NUMBER martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}",method="GET",status="200",le="0.01"} NUMBER martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}",method="GET",status="200",le="0.025"} NUMBER @@ -154,16 +252,68 @@ martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y} martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}",method="GET",status="200",le="+Inf"} NUMBER martin_http_requests_duration_seconds_sum{endpoint="/{source_ids}/{z}/{x}/{y}",method="GET",status="200"} NUMBER martin_http_requests_duration_seconds_count{endpoint="/{source_ids}/{z}/{x}/{y}",method="GET",status="200"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="0.005"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="0.01"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="0.025"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="0.05"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="0.1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="0.25"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="0.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="2.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="10"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="+Inf"} NUMBER +martin_http_requests_duration_seconds_sum{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301"} NUMBER +martin_http_requests_duration_seconds_count{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="0.005"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="0.01"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="0.025"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="0.05"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="0.1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="0.25"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="0.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="2.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="10"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="+Inf"} NUMBER +martin_http_requests_duration_seconds_sum{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301"} NUMBER +martin_http_requests_duration_seconds_count{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="0.005"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="0.01"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="0.025"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="0.05"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="0.1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="0.25"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="0.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="2.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="10"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="+Inf"} NUMBER +martin_http_requests_duration_seconds_sum{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301"} NUMBER +martin_http_requests_duration_seconds_count{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301"} NUMBER # HELP martin_http_requests_total Total number of HTTP requests # TYPE martin_http_requests_total counter martin_http_requests_total{endpoint="/_/metrics",method="GET",status="200"} NUMBER martin_http_requests_total{endpoint="/catalog",method="GET",status="200"} NUMBER martin_http_requests_total{endpoint="/font/{fontstack}/{start}-{end}",method="GET",status="200"} NUMBER +martin_http_requests_total{endpoint="/fonts/{fontstack}/{start}-{end}",method="HEAD",status="301"} NUMBER martin_http_requests_total{endpoint="/health",method="GET",status="200"} NUMBER martin_http_requests_total{endpoint="/sdf_sprite/{source_ids}.json",method="GET",status="200"} NUMBER martin_http_requests_total{endpoint="/sdf_sprite/{source_ids}.png",method="GET",status="200"} NUMBER +martin_http_requests_total{endpoint="/sdf_sprites/{source_ids}.json",method="HEAD",status="301"} NUMBER +martin_http_requests_total{endpoint="/sdf_sprites/{source_ids}.png",method="HEAD",status="301"} NUMBER martin_http_requests_total{endpoint="/sprite/{source_ids}.json",method="GET",status="200"} NUMBER martin_http_requests_total{endpoint="/sprite/{source_ids}.png",method="GET",status="200"} NUMBER +martin_http_requests_total{endpoint="/sprites/{source_ids}.json",method="HEAD",status="301"} NUMBER +martin_http_requests_total{endpoint="/sprites/{source_ids}.png",method="HEAD",status="301"} NUMBER martin_http_requests_total{endpoint="/style/{style_id}",method="GET",status="200"} NUMBER +martin_http_requests_total{endpoint="/styles/{style_id}",method="HEAD",status="301"} NUMBER +martin_http_requests_total{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301"} NUMBER martin_http_requests_total{endpoint="/{source_ids}",method="GET",status="200"} NUMBER martin_http_requests_total{endpoint="/{source_ids}/{z}/{x}/{y}",method="GET",status="200"} NUMBER +martin_http_requests_total{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301"} NUMBER +martin_http_requests_total{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301"} NUMBER +martin_http_requests_total{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301"} NUMBER diff --git a/tests/expected/configured/metrics_1.txt b/tests/expected/configured/metrics_1.txt index 47ea18713..9f55b4016 100644 --- a/tests/expected/configured/metrics_1.txt +++ b/tests/expected/configured/metrics_1.txt @@ -28,6 +28,20 @@ martin_http_requests_duration_seconds_bucket{endpoint="/font/{fontstack}/{start} martin_http_requests_duration_seconds_bucket{endpoint="/font/{fontstack}/{start}-{end}",method="GET",status="200",le="+Inf"} NUMBER martin_http_requests_duration_seconds_sum{endpoint="/font/{fontstack}/{start}-{end}",method="GET",status="200"} NUMBER martin_http_requests_duration_seconds_count{endpoint="/font/{fontstack}/{start}-{end}",method="GET",status="200"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/fonts/{fontstack}/{start}-{end}",method="HEAD",status="301",le="0.005"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/fonts/{fontstack}/{start}-{end}",method="HEAD",status="301",le="0.01"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/fonts/{fontstack}/{start}-{end}",method="HEAD",status="301",le="0.025"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/fonts/{fontstack}/{start}-{end}",method="HEAD",status="301",le="0.05"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/fonts/{fontstack}/{start}-{end}",method="HEAD",status="301",le="0.1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/fonts/{fontstack}/{start}-{end}",method="HEAD",status="301",le="0.25"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/fonts/{fontstack}/{start}-{end}",method="HEAD",status="301",le="0.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/fonts/{fontstack}/{start}-{end}",method="HEAD",status="301",le="1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/fonts/{fontstack}/{start}-{end}",method="HEAD",status="301",le="2.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/fonts/{fontstack}/{start}-{end}",method="HEAD",status="301",le="5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/fonts/{fontstack}/{start}-{end}",method="HEAD",status="301",le="10"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/fonts/{fontstack}/{start}-{end}",method="HEAD",status="301",le="+Inf"} NUMBER +martin_http_requests_duration_seconds_sum{endpoint="/fonts/{fontstack}/{start}-{end}",method="HEAD",status="301"} NUMBER +martin_http_requests_duration_seconds_count{endpoint="/fonts/{fontstack}/{start}-{end}",method="HEAD",status="301"} NUMBER martin_http_requests_duration_seconds_bucket{endpoint="/health",method="GET",status="200",le="0.005"} NUMBER martin_http_requests_duration_seconds_bucket{endpoint="/health",method="GET",status="200",le="0.01"} NUMBER martin_http_requests_duration_seconds_bucket{endpoint="/health",method="GET",status="200",le="0.025"} NUMBER @@ -70,6 +84,34 @@ martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprite/{source_ids}. martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprite/{source_ids}.png",method="GET",status="200",le="+Inf"} NUMBER martin_http_requests_duration_seconds_sum{endpoint="/sdf_sprite/{source_ids}.png",method="GET",status="200"} NUMBER martin_http_requests_duration_seconds_count{endpoint="/sdf_sprite/{source_ids}.png",method="GET",status="200"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.json",method="HEAD",status="301",le="0.005"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.json",method="HEAD",status="301",le="0.01"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.json",method="HEAD",status="301",le="0.025"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.json",method="HEAD",status="301",le="0.05"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.json",method="HEAD",status="301",le="0.1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.json",method="HEAD",status="301",le="0.25"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.json",method="HEAD",status="301",le="0.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.json",method="HEAD",status="301",le="1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.json",method="HEAD",status="301",le="2.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.json",method="HEAD",status="301",le="5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.json",method="HEAD",status="301",le="10"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.json",method="HEAD",status="301",le="+Inf"} NUMBER +martin_http_requests_duration_seconds_sum{endpoint="/sdf_sprites/{source_ids}.json",method="HEAD",status="301"} NUMBER +martin_http_requests_duration_seconds_count{endpoint="/sdf_sprites/{source_ids}.json",method="HEAD",status="301"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.png",method="HEAD",status="301",le="0.005"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.png",method="HEAD",status="301",le="0.01"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.png",method="HEAD",status="301",le="0.025"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.png",method="HEAD",status="301",le="0.05"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.png",method="HEAD",status="301",le="0.1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.png",method="HEAD",status="301",le="0.25"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.png",method="HEAD",status="301",le="0.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.png",method="HEAD",status="301",le="1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.png",method="HEAD",status="301",le="2.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.png",method="HEAD",status="301",le="5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.png",method="HEAD",status="301",le="10"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sdf_sprites/{source_ids}.png",method="HEAD",status="301",le="+Inf"} NUMBER +martin_http_requests_duration_seconds_sum{endpoint="/sdf_sprites/{source_ids}.png",method="HEAD",status="301"} NUMBER +martin_http_requests_duration_seconds_count{endpoint="/sdf_sprites/{source_ids}.png",method="HEAD",status="301"} NUMBER martin_http_requests_duration_seconds_bucket{endpoint="/sprite/{source_ids}.json",method="GET",status="200",le="0.005"} NUMBER martin_http_requests_duration_seconds_bucket{endpoint="/sprite/{source_ids}.json",method="GET",status="200",le="0.01"} NUMBER martin_http_requests_duration_seconds_bucket{endpoint="/sprite/{source_ids}.json",method="GET",status="200",le="0.025"} NUMBER @@ -98,6 +140,34 @@ martin_http_requests_duration_seconds_bucket{endpoint="/sprite/{source_ids}.png" martin_http_requests_duration_seconds_bucket{endpoint="/sprite/{source_ids}.png",method="GET",status="200",le="+Inf"} NUMBER martin_http_requests_duration_seconds_sum{endpoint="/sprite/{source_ids}.png",method="GET",status="200"} NUMBER martin_http_requests_duration_seconds_count{endpoint="/sprite/{source_ids}.png",method="GET",status="200"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.json",method="HEAD",status="301",le="0.005"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.json",method="HEAD",status="301",le="0.01"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.json",method="HEAD",status="301",le="0.025"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.json",method="HEAD",status="301",le="0.05"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.json",method="HEAD",status="301",le="0.1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.json",method="HEAD",status="301",le="0.25"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.json",method="HEAD",status="301",le="0.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.json",method="HEAD",status="301",le="1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.json",method="HEAD",status="301",le="2.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.json",method="HEAD",status="301",le="5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.json",method="HEAD",status="301",le="10"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.json",method="HEAD",status="301",le="+Inf"} NUMBER +martin_http_requests_duration_seconds_sum{endpoint="/sprites/{source_ids}.json",method="HEAD",status="301"} NUMBER +martin_http_requests_duration_seconds_count{endpoint="/sprites/{source_ids}.json",method="HEAD",status="301"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.png",method="HEAD",status="301",le="0.005"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.png",method="HEAD",status="301",le="0.01"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.png",method="HEAD",status="301",le="0.025"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.png",method="HEAD",status="301",le="0.05"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.png",method="HEAD",status="301",le="0.1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.png",method="HEAD",status="301",le="0.25"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.png",method="HEAD",status="301",le="0.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.png",method="HEAD",status="301",le="1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.png",method="HEAD",status="301",le="2.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.png",method="HEAD",status="301",le="5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.png",method="HEAD",status="301",le="10"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/sprites/{source_ids}.png",method="HEAD",status="301",le="+Inf"} NUMBER +martin_http_requests_duration_seconds_sum{endpoint="/sprites/{source_ids}.png",method="HEAD",status="301"} NUMBER +martin_http_requests_duration_seconds_count{endpoint="/sprites/{source_ids}.png",method="HEAD",status="301"} NUMBER martin_http_requests_duration_seconds_bucket{endpoint="/style/{style_id}",method="GET",status="200",le="0.005"} NUMBER martin_http_requests_duration_seconds_bucket{endpoint="/style/{style_id}",method="GET",status="200",le="0.01"} NUMBER martin_http_requests_duration_seconds_bucket{endpoint="/style/{style_id}",method="GET",status="200",le="0.025"} NUMBER @@ -112,6 +182,34 @@ martin_http_requests_duration_seconds_bucket{endpoint="/style/{style_id}",method martin_http_requests_duration_seconds_bucket{endpoint="/style/{style_id}",method="GET",status="200",le="+Inf"} NUMBER martin_http_requests_duration_seconds_sum{endpoint="/style/{style_id}",method="GET",status="200"} NUMBER martin_http_requests_duration_seconds_count{endpoint="/style/{style_id}",method="GET",status="200"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/styles/{style_id}",method="HEAD",status="301",le="0.005"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/styles/{style_id}",method="HEAD",status="301",le="0.01"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/styles/{style_id}",method="HEAD",status="301",le="0.025"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/styles/{style_id}",method="HEAD",status="301",le="0.05"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/styles/{style_id}",method="HEAD",status="301",le="0.1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/styles/{style_id}",method="HEAD",status="301",le="0.25"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/styles/{style_id}",method="HEAD",status="301",le="0.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/styles/{style_id}",method="HEAD",status="301",le="1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/styles/{style_id}",method="HEAD",status="301",le="2.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/styles/{style_id}",method="HEAD",status="301",le="5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/styles/{style_id}",method="HEAD",status="301",le="10"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/styles/{style_id}",method="HEAD",status="301",le="+Inf"} NUMBER +martin_http_requests_duration_seconds_sum{endpoint="/styles/{style_id}",method="HEAD",status="301"} NUMBER +martin_http_requests_duration_seconds_count{endpoint="/styles/{style_id}",method="HEAD",status="301"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301",le="0.005"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301",le="0.01"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301",le="0.025"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301",le="0.05"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301",le="0.1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301",le="0.25"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301",le="0.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301",le="1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301",le="2.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301",le="5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301",le="10"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301",le="+Inf"} NUMBER +martin_http_requests_duration_seconds_sum{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301"} NUMBER +martin_http_requests_duration_seconds_count{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301"} NUMBER martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}",method="GET",status="200",le="0.005"} NUMBER martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}",method="GET",status="200",le="0.01"} NUMBER martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}",method="GET",status="200",le="0.025"} NUMBER @@ -140,15 +238,67 @@ martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y} martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}",method="GET",status="200",le="+Inf"} NUMBER martin_http_requests_duration_seconds_sum{endpoint="/{source_ids}/{z}/{x}/{y}",method="GET",status="200"} NUMBER martin_http_requests_duration_seconds_count{endpoint="/{source_ids}/{z}/{x}/{y}",method="GET",status="200"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="0.005"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="0.01"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="0.025"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="0.05"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="0.1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="0.25"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="0.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="2.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="10"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="+Inf"} NUMBER +martin_http_requests_duration_seconds_sum{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301"} NUMBER +martin_http_requests_duration_seconds_count{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="0.005"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="0.01"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="0.025"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="0.05"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="0.1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="0.25"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="0.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="2.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="10"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="+Inf"} NUMBER +martin_http_requests_duration_seconds_sum{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301"} NUMBER +martin_http_requests_duration_seconds_count{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="0.005"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="0.01"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="0.025"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="0.05"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="0.1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="0.25"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="0.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="2.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="10"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="+Inf"} NUMBER +martin_http_requests_duration_seconds_sum{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301"} NUMBER +martin_http_requests_duration_seconds_count{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301"} NUMBER # HELP martin_http_requests_total Total number of HTTP requests # TYPE martin_http_requests_total counter martin_http_requests_total{endpoint="/catalog",method="GET",status="200"} NUMBER martin_http_requests_total{endpoint="/font/{fontstack}/{start}-{end}",method="GET",status="200"} NUMBER +martin_http_requests_total{endpoint="/fonts/{fontstack}/{start}-{end}",method="HEAD",status="301"} NUMBER martin_http_requests_total{endpoint="/health",method="GET",status="200"} NUMBER martin_http_requests_total{endpoint="/sdf_sprite/{source_ids}.json",method="GET",status="200"} NUMBER martin_http_requests_total{endpoint="/sdf_sprite/{source_ids}.png",method="GET",status="200"} NUMBER +martin_http_requests_total{endpoint="/sdf_sprites/{source_ids}.json",method="HEAD",status="301"} NUMBER +martin_http_requests_total{endpoint="/sdf_sprites/{source_ids}.png",method="HEAD",status="301"} NUMBER martin_http_requests_total{endpoint="/sprite/{source_ids}.json",method="GET",status="200"} NUMBER martin_http_requests_total{endpoint="/sprite/{source_ids}.png",method="GET",status="200"} NUMBER +martin_http_requests_total{endpoint="/sprites/{source_ids}.json",method="HEAD",status="301"} NUMBER +martin_http_requests_total{endpoint="/sprites/{source_ids}.png",method="HEAD",status="301"} NUMBER martin_http_requests_total{endpoint="/style/{style_id}",method="GET",status="200"} NUMBER +martin_http_requests_total{endpoint="/styles/{style_id}",method="HEAD",status="301"} NUMBER +martin_http_requests_total{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301"} NUMBER martin_http_requests_total{endpoint="/{source_ids}",method="GET",status="200"} NUMBER martin_http_requests_total{endpoint="/{source_ids}/{z}/{x}/{y}",method="GET",status="200"} NUMBER +martin_http_requests_total{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301"} NUMBER +martin_http_requests_total{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301"} NUMBER +martin_http_requests_total{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301"} NUMBER diff --git a/tests/test.sh b/tests/test.sh index 051ace66f..323623b33 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -586,23 +586,23 @@ test_font font_3 font/Overpass%20Mono%20Regular,Overpass%20Mono%20Light/0-2 >&2 echo "***** Test URL redirects (HTTP 301) *****" # Test pluralization redirects -test_redirect styles/maplibre /style/maplibre -test_redirect sprites/src1.json /sprite/src1.json -test_redirect sprites/src1.png /sprite/src1.png -test_redirect sdf_sprites/src1.json /sdf_sprite/src1.json -test_redirect sdf_sprites/src1.png /sdf_sprite/src1.png -test_redirect "fonts/Overpass%20Mono%20Regular/0-255" "/font/Overpass%20Mono%20Regular/0-255" +test_redirect styles/maplibre /style/maplibre +test_redirect sprites/src1.json /sprite/src1.json +test_redirect sprites/src1.png /sprite/src1.png +test_redirect sdf_sprites/src1.json /sdf_sprite/src1.json +test_redirect sdf_sprites/src1.png /sdf_sprite/src1.png +test_redirect "fonts/Overpass%20Mono%20Regular/0-255" "/font/Overpass Mono Regular/0-255" # Test tile format suffix redirects -test_redirect stamen_toner__raster_CC-BY-ODbL_z3/0/0/0.pbf /stamen_toner__raster_CC-BY-ODbL_z3/0/0/0 -test_redirect stamen_toner__raster_CC-BY-ODbL_z3/0/0/0.mvt /stamen_toner__raster_CC-BY-ODbL_z3/0/0/0 -test_redirect stamen_toner__raster_CC-BY-ODbL_z3/0/0/0.mlt /stamen_toner__raster_CC-BY-ODbL_z3/0/0/0 +test_redirect table_source/0/0/0.pbf /table_source/0/0/0 +test_redirect table_source/0/0/0.mvt /table_source/0/0/0 +test_redirect table_source/0/0/0.mlt /table_source/0/0/0 # Test /tiles/ prefix redirect -test_redirect tiles/stamen_toner__raster_CC-BY-ODbL_z3/0/0/0 /stamen_toner__raster_CC-BY-ODbL_z3/0/0/0 +test_redirect tiles/table_source/0/0/0 /table_source/0/0/0 # Test query string preservation for tiles -test_redirect "stamen_toner__raster_CC-BY-ODbL_z3/0/0/0.pbf?test=123" "/stamen_toner__raster_CC-BY-ODbL_z3/0/0/0?test=123" +test_redirect "table_source/0/0/0.pbf?test=123" "/table_source/0/0/0?test=123" # Test comments override test_jsn tbl_comment_cfg MixPoints From 20c29ad7b23651e14ca1da923083556684c87347 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Wed, 28 Jan 2026 11:23:50 +0100 Subject: [PATCH 21/37] move to a more sensible order --- tests/test.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test.sh b/tests/test.sh index 323623b33..1a8f33278 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -583,6 +583,10 @@ test_font font_1 font/Overpass%20Mono%20Light/0-255 test_font font_2 font/Overpass%20Mono%20Regular/0-255 test_font font_3 font/Overpass%20Mono%20Regular,Overpass%20Mono%20Light/0-255 +# Test comments override +test_jsn tbl_comment_cfg MixPoints +test_jsn fnc_comment_cfg function_Mixed_Name + >&2 echo "***** Test URL redirects (HTTP 301) *****" # Test pluralization redirects @@ -604,9 +608,7 @@ test_redirect tiles/table_source/0/0/0 /table_source/0/0/0 # Test query string preservation for tiles test_redirect "table_source/0/0/0.pbf?test=123" "/table_source/0/0/0?test=123" -# Test comments override -test_jsn tbl_comment_cfg MixPoints -test_jsn fnc_comment_cfg function_Mixed_Name +>&2 echo "***** Test observability outputs (metrics, logs) *****" test_metrics "metrics_1" From 5f7d1929552f0c27d43d7b3e53d33bfe18b7d5f3 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Wed, 28 Jan 2026 23:38:23 +0100 Subject: [PATCH 22/37] add a debounced warning --- martin/src/srv/fonts.rs | 14 ++++++++ martin/src/srv/server.rs | 7 ++-- martin/src/srv/sprites.rs | 60 ++++++++++++++++++++++++++++++--- martin/src/srv/styles.rs | 10 ++++++ martin/src/srv/tiles/content.rs | 59 +++++++++++++++++++++++--------- 5 files changed, 125 insertions(+), 25 deletions(-) diff --git a/martin/src/srv/fonts.rs b/martin/src/srv/fonts.rs index 130ae7f5d..41ea4abb1 100644 --- a/martin/src/srv/fonts.rs +++ b/martin/src/srv/fonts.rs @@ -1,4 +1,6 @@ use std::string::ToString; +use std::sync::LazyLock; +use std::time::{Duration, Instant}; use actix_middleware_etag::Etag; use actix_web::error::{ErrorBadRequest, ErrorNotFound}; @@ -8,6 +10,8 @@ use actix_web::web::{Data, Path}; use actix_web::{HttpResponse, Result as ActixResult, route}; use martin_core::fonts::{FontError, FontSources, OptFontCache}; use serde::Deserialize; +use tokio::sync::Mutex; +use tracing::warn; use crate::srv::server::map_internal_error; @@ -47,6 +51,16 @@ async fn get_font( /// Redirect `/fonts/{fontstack}/{start}-{end}` to `/font/{fontstack}/{start}-{end}` (HTTP 301) #[route("/fonts/{fontstack}/{start}-{end}", method = "GET", method = "HEAD")] pub async fn redirect_fonts(path: Path) -> HttpResponse { + static LAST_WARNING: LazyLock> = LazyLock::new(|| Mutex::new(Instant::now())); + + let mut warning = LAST_WARNING.lock().await; + if warning.elapsed() >= Duration::from_hours(1) { + *warning = Instant::now(); + warn!( + "Using /fonts/{{fontstack}}/{{start}}-{{end}} endpoint which causes an unnecessary redirect. Use /font/{{fontstack}}/{{start}}-{{end}} directly to avoid extra round-trip latency." + ); + } + HttpResponse::MovedPermanently() .insert_header(( LOCATION, diff --git a/martin/src/srv/server.rs b/martin/src/srv/server.rs index 2981913ec..09484ac48 100644 --- a/martin/src/srv/server.rs +++ b/martin/src/srv/server.rs @@ -59,11 +59,8 @@ fn register_services(cfg: &mut web::ServiceConfig, #[allow(unused_variables)] us { // Register tile format suffix redirects BEFORE the main tile route // because Actix-Web matches routes in registration order - cfg.service(crate::srv::tiles::content::redirect_tile_pbf) - .service(crate::srv::tiles::content::redirect_tile_mvt) - .service(crate::srv::tiles::content::redirect_tile_mlt); - - cfg.service(crate::srv::tiles::metadata::get_source_info) + cfg.service(crate::srv::tiles::content::redirect_tile_ext) + .service(crate::srv::tiles::metadata::get_source_info) .service(crate::srv::tiles::content::get_tile); // Register /tiles/ prefix redirect after main tile route diff --git a/martin/src/srv/sprites.rs b/martin/src/srv/sprites.rs index b2d58e0e6..290e454b1 100644 --- a/martin/src/srv/sprites.rs +++ b/martin/src/srv/sprites.rs @@ -1,4 +1,6 @@ use std::string::ToString; +use std::sync::LazyLock; +use std::time::{Duration, Instant}; use actix_middleware_etag::Etag; use actix_web::error::ErrorNotFound; @@ -8,6 +10,8 @@ use actix_web::web::{Bytes, Data, Path}; use actix_web::{HttpResponse, Result as ActixResult, route}; use martin_core::sprites::{OptSpriteCache, SpriteError, SpriteSources}; use serde::Deserialize; +use tokio::sync::Mutex; +use tracing::warn; use crate::srv::server::map_internal_error; @@ -45,8 +49,20 @@ async fn get_sprite_png( /// Redirect `/sprites/{source_ids}.png` to `/sprite/{source_ids}.png` (HTTP 301) #[route("/sprites/{source_ids}.png", method = "GET", method = "HEAD")] pub async fn redirect_sprites_png(path: Path) -> HttpResponse { + let SourceIDsRequest { source_ids } = path.as_ref(); + + static LAST_WARNING: LazyLock> = LazyLock::new(|| Mutex::new(Instant::now())); + + let mut warning = LAST_WARNING.lock().await; + if warning.elapsed() >= Duration::from_hours(1) { + *warning = Instant::now(); + warn!( + "Using /sprites/{source_ids}.png endpoint which causes an unnecessary redirect. Use /sprite/{source_ids}.png directly to avoid extra round-trip latency." + ); + } + HttpResponse::MovedPermanently() - .insert_header((LOCATION, format!("/sprite/{}.png", path.source_ids))) + .insert_header((LOCATION, format!("/sprite/{source_ids}.png"))) .finish() } @@ -79,8 +95,20 @@ async fn get_sprite_sdf_png( /// Redirect `/sdf_sprites/{source_ids}.png` to `/sdf_sprite/{source_ids}.png` (HTTP 301) #[route("/sdf_sprites/{source_ids}.png", method = "GET", method = "HEAD")] pub async fn redirect_sdf_sprites_png(path: Path) -> HttpResponse { + let SourceIDsRequest { source_ids } = path.as_ref(); + + static LAST_WARNING: LazyLock> = LazyLock::new(|| Mutex::new(Instant::now())); + + let mut warning = LAST_WARNING.lock().await; + if warning.elapsed() >= Duration::from_hours(1) { + *warning = Instant::now(); + warn!( + "Using /sdf_sprites/{source_ids}.png endpoint which causes an unnecessary redirect. Use /sdf_sprite/{source_ids}.png directly to avoid extra round-trip latency." + ); + } + HttpResponse::MovedPermanently() - .insert_header((LOCATION, format!("/sdf_sprite/{}.png", path.source_ids))) + .insert_header((LOCATION, format!("/sdf_sprite/{source_ids}.png"))) .finish() } @@ -114,8 +142,20 @@ async fn get_sprite_json( /// Redirect `/sprites/{source_ids}.json` to `/sprite/{source_ids}.json` (HTTP 301) #[route("/sprites/{source_ids}.json", method = "GET", method = "HEAD")] pub async fn redirect_sprites_json(path: Path) -> HttpResponse { + let SourceIDsRequest { source_ids } = path.as_ref(); + + static LAST_WARNING: LazyLock> = LazyLock::new(|| Mutex::new(Instant::now())); + + let mut warning = LAST_WARNING.lock().await; + if warning.elapsed() >= Duration::from_hours(1) { + *warning = Instant::now(); + warn!( + "Using /sprites/{source_ids}.json endpoint which causes an unnecessary redirect. Use /sprite/{source_ids}.json directly to avoid extra round-trip latency." + ); + } + HttpResponse::MovedPermanently() - .insert_header((LOCATION, format!("/sprite/{}.json", path.source_ids))) + .insert_header((LOCATION, format!("/sprite/{source_ids}.json"))) .finish() } @@ -149,8 +189,20 @@ async fn get_sprite_sdf_json( /// Redirect `/sdf_sprites/{source_ids}.json` to `/sdf_sprite/{source_ids}.json` (HTTP 301) #[route("/sdf_sprites/{source_ids}.json", method = "GET", method = "HEAD")] pub async fn redirect_sdf_sprites_json(path: Path) -> HttpResponse { + let SourceIDsRequest { source_ids } = path.as_ref(); + + static LAST_WARNING: LazyLock> = LazyLock::new(|| Mutex::new(Instant::now())); + + let mut warning = LAST_WARNING.lock().await; + if warning.elapsed() >= Duration::from_hours(1) { + *warning = Instant::now(); + warn!( + "Using /sdf_sprites/{source_ids}.json endpoint which causes an unnecessary redirect. Use /sdf_sprite/{source_ids}.json directly to avoid extra round-trip latency." + ); + } + HttpResponse::MovedPermanently() - .insert_header((LOCATION, format!("/sdf_sprite/{}.json", path.source_ids))) + .insert_header((LOCATION, format!("/sdf_sprite/{source_ids}.json"))) .finish() } diff --git a/martin/src/srv/styles.rs b/martin/src/srv/styles.rs index bd3d6c0a1..73f9f2b0f 100644 --- a/martin/src/srv/styles.rs +++ b/martin/src/srv/styles.rs @@ -53,6 +53,16 @@ async fn get_style_json(path: Path, styles: Data) -> /// This handles common pluralization mistakes #[route("/styles/{style_id}", method = "GET", method = "HEAD")] pub(crate) async fn redirect_styles(path: Path) -> HttpResponse { + static LAST_WARNING: LazyLock> = LazyLock::new(|| Mutex::new(Instant::now())); + + let mut warning = LAST_WARNING.lock().await; + if warning.elapsed() >= Duration::from_hours(1) { + *warning = Instant::now(); + warn!( + "Using /fonts/{{fontstack}}/{{start}}-{{end}} endpoint which causes an unnecessary redirect. Use /font/{{fontstack}}/{{start}}-{{end}} directly to avoid extra round-trip latency." + ); + } + HttpResponse::MovedPermanently() .insert_header((LOCATION, format!("/style/{}", path.style_id))) .finish() diff --git a/martin/src/srv/tiles/content.rs b/martin/src/srv/tiles/content.rs index 670857c0c..1f6aca2e7 100644 --- a/martin/src/srv/tiles/content.rs +++ b/martin/src/srv/tiles/content.rs @@ -1,3 +1,6 @@ +use std::sync::LazyLock; +use std::time::{Duration, Instant}; + use actix_http::ContentEncoding; use actix_http::header::Quality; use actix_web::error::{ErrorBadRequest, ErrorNotAcceptable, ErrorNotFound}; @@ -14,6 +17,8 @@ use martin_tile_utils::{ encode_gzip, }; use serde::Deserialize; +use tokio::sync::Mutex; +use tracing::warn; use crate::config::args::PreferredEncoding; use crate::config::file::srv::SrvConfig; @@ -61,31 +66,53 @@ async fn get_tile( .await } -/// Redirect `/{source_ids}/{z}/{x}/{y}.pbf` to `/{source_ids}/{z}/{x}/{y}` (HTTP 301) -/// Registered before main tile route to match more specific pattern first -#[route("/{source_ids}/{z}/{x}/{y}.pbf", method = "GET", method = "HEAD")] -pub async fn redirect_tile_pbf(req: HttpRequest, path: Path) -> HttpResponse { - redirect_tile_with_query(&path.source_ids, path.z, path.x, path.y, req.query_string()) +#[derive(Deserialize, Clone)] +pub struct RedirectTileRequest { + ids: String, + z: u8, + x: u32, + y: u32, + ext: String, } -/// Redirect `/{source_ids}/{z}/{x}/{y}.mvt` to `/{source_ids}/{z}/{x}/{y}` (HTTP 301) +/// Redirect `/{source_ids}/{z}/{x}/{y}.{extension}` to `/{source_ids}/{z}/{x}/{y}` (HTTP 301) /// Registered before main tile route to match more specific pattern first -#[route("/{source_ids}/{z}/{x}/{y}.mvt", method = "GET", method = "HEAD")] -pub async fn redirect_tile_mvt(req: HttpRequest, path: Path) -> HttpResponse { - redirect_tile_with_query(&path.source_ids, path.z, path.x, path.y, req.query_string()) -} +#[route("/{ids}/{z}/{x}/{y}.{ext}", method = "GET", method = "HEAD")] +pub async fn redirect_tile_ext(req: HttpRequest, path: Path) -> HttpResponse { + let RedirectTileRequest { ids, z, x, y, ext } = path.as_ref(); + static LAST_WARNING: LazyLock> = LazyLock::new(|| Mutex::new(Instant::now())); + + let mut warning = LAST_WARNING.lock().await; + if warning.elapsed() >= Duration::from_hours(1) { + *warning = Instant::now(); + warn!( + "Using /{ids}/{z}/{x}/{y}.{ext} endpoint which causes an unnecessary redirect. Use /{ids}/{z}/{x}/{y} directly to avoid extra round-trip latency.", + ); + } -/// Redirect `/{source_ids}/{z}/{x}/{y}.mlt` to `/{source_ids}/{z}/{x}/{y}` (HTTP 301) -/// Registered before main tile route to match more specific pattern first -#[route("/{source_ids}/{z}/{x}/{y}.mlt", method = "GET", method = "HEAD")] -pub async fn redirect_tile_mlt(req: HttpRequest, path: Path) -> HttpResponse { - redirect_tile_with_query(&path.source_ids, path.z, path.x, path.y, req.query_string()) + redirect_tile_with_query(ids, *z, *x, *y, req.query_string()) } /// Redirect `/tiles/{source_ids}/{z}/{x}/{y}` to `/{source_ids}/{z}/{x}/{y}` (HTTP 301) #[route("/tiles/{source_ids}/{z}/{x}/{y}", method = "GET", method = "HEAD")] pub async fn redirect_tiles(req: HttpRequest, path: Path) -> HttpResponse { - redirect_tile_with_query(&path.source_ids, path.z, path.x, path.y, req.query_string()) + let TileRequest { + source_ids, + z, + x, + y, + } = path.as_ref(); + static LAST_WARNING: LazyLock> = LazyLock::new(|| Mutex::new(Instant::now())); + + let mut warning = LAST_WARNING.lock().await; + if warning.elapsed() >= Duration::from_hours(1) { + *warning = Instant::now(); + warn!( + "Using /tiles/{source_ids}/{z}/{x}/{y} endpoint which causes an unnecessary redirect. Use /{source_ids}/{z}/{x}/{y} directly to avoid extra round-trip latency.", + ); + } + + redirect_tile_with_query(source_ids, *z, *x, *y, req.query_string()) } /// Helper function to create a 301 redirect for tiles with query string preservation From f6de020882decbaddf517dd0ce1a10a975817ce3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 22:40:34 +0000 Subject: [PATCH 23/37] chore(fmt): apply pre-commit formatting fixes --- martin/src/srv/styles.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/martin/src/srv/styles.rs b/martin/src/srv/styles.rs index 73f9f2b0f..8f8289018 100644 --- a/martin/src/srv/styles.rs +++ b/martin/src/srv/styles.rs @@ -53,16 +53,16 @@ async fn get_style_json(path: Path, styles: Data) -> /// This handles common pluralization mistakes #[route("/styles/{style_id}", method = "GET", method = "HEAD")] pub(crate) async fn redirect_styles(path: Path) -> HttpResponse { - static LAST_WARNING: LazyLock> = LazyLock::new(|| Mutex::new(Instant::now())); + static LAST_WARNING: LazyLock> = LazyLock::new(|| Mutex::new(Instant::now())); + + let mut warning = LAST_WARNING.lock().await; + if warning.elapsed() >= Duration::from_hours(1) { + *warning = Instant::now(); + warn!( + "Using /fonts/{{fontstack}}/{{start}}-{{end}} endpoint which causes an unnecessary redirect. Use /font/{{fontstack}}/{{start}}-{{end}} directly to avoid extra round-trip latency." + ); + } - let mut warning = LAST_WARNING.lock().await; - if warning.elapsed() >= Duration::from_hours(1) { - *warning = Instant::now(); - warn!( - "Using /fonts/{{fontstack}}/{{start}}-{{end}} endpoint which causes an unnecessary redirect. Use /font/{{fontstack}}/{{start}}-{{end}} directly to avoid extra round-trip latency." - ); - } - HttpResponse::MovedPermanently() .insert_header((LOCATION, format!("/style/{}", path.style_id))) .finish() From 963703a9498f5fea156ee3d3da1d30b2e994c032 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 23:05:54 +0000 Subject: [PATCH 24/37] Refactor redirect warnings to use DebouncedWarning helper - Created DebouncedWarning struct in server.rs to eliminate code duplication - Updated all redirect handlers (tiles, sprites, fonts, styles) to use the helper - Fixed warning message for /tiles/ redirect per review feedback - All warnings now use consistent "Request to X caused unnecessary redirect" format - Warnings debounced to once per hour per redirect type Co-authored-by: nyurik <1641515+nyurik@users.noreply.github.com> --- martin/src/srv/fonts.rs | 20 ++++------ martin/src/srv/server.rs | 28 ++++++++++++++ martin/src/srv/sprites.rs | 65 +++++++++++++-------------------- martin/src/srv/styles.rs | 19 ++++++---- martin/src/srv/tiles/content.rs | 32 +++++++--------- 5 files changed, 85 insertions(+), 79 deletions(-) diff --git a/martin/src/srv/fonts.rs b/martin/src/srv/fonts.rs index 41ea4abb1..d30cc5924 100644 --- a/martin/src/srv/fonts.rs +++ b/martin/src/srv/fonts.rs @@ -1,6 +1,5 @@ use std::string::ToString; use std::sync::LazyLock; -use std::time::{Duration, Instant}; use actix_middleware_etag::Etag; use actix_web::error::{ErrorBadRequest, ErrorNotFound}; @@ -10,10 +9,8 @@ use actix_web::web::{Data, Path}; use actix_web::{HttpResponse, Result as ActixResult, route}; use martin_core::fonts::{FontError, FontSources, OptFontCache}; use serde::Deserialize; -use tokio::sync::Mutex; -use tracing::warn; -use crate::srv::server::map_internal_error; +use crate::srv::server::{DebouncedWarning, map_internal_error}; #[derive(Deserialize, Debug)] struct FontRequest { @@ -51,15 +48,14 @@ async fn get_font( /// Redirect `/fonts/{fontstack}/{start}-{end}` to `/font/{fontstack}/{start}-{end}` (HTTP 301) #[route("/fonts/{fontstack}/{start}-{end}", method = "GET", method = "HEAD")] pub async fn redirect_fonts(path: Path) -> HttpResponse { - static LAST_WARNING: LazyLock> = LazyLock::new(|| Mutex::new(Instant::now())); + static WARNING: LazyLock = LazyLock::new(DebouncedWarning::new); - let mut warning = LAST_WARNING.lock().await; - if warning.elapsed() >= Duration::from_hours(1) { - *warning = Instant::now(); - warn!( - "Using /fonts/{{fontstack}}/{{start}}-{{end}} endpoint which causes an unnecessary redirect. Use /font/{{fontstack}}/{{start}}-{{end}} directly to avoid extra round-trip latency." - ); - } + WARNING + .warn_once_per_hour(&format!( + "Request to /fonts/{}/{}-{} caused unnecessary redirect. Use /font/{}/{}-{} to avoid extra round-trip latency.", + path.fontstack, path.start, path.end, path.fontstack, path.start, path.end + )) + .await; HttpResponse::MovedPermanently() .insert_header(( diff --git a/martin/src/srv/server.rs b/martin/src/srv/server.rs index 09484ac48..3f6807408 100644 --- a/martin/src/srv/server.rs +++ b/martin/src/srv/server.rs @@ -33,6 +33,34 @@ pub fn map_internal_error(e: T) -> actix_web::Error { actix_web::error::ErrorInternalServerError(e.to_string()) } +/// Helper struct for debounced warning messages in redirect handlers. +/// Ensures warnings are logged no more than once per hour to avoid log spam. +#[cfg(any(feature = "_tiles", feature = "fonts", feature = "sprites"))] +pub struct DebouncedWarning { + last_warning: std::sync::LazyLock>, +} + +#[cfg(any(feature = "_tiles", feature = "fonts", feature = "sprites"))] +impl DebouncedWarning { + /// Create a new `DebouncedWarning` instance + pub const fn new() -> Self { + Self { + last_warning: std::sync::LazyLock::new(|| { + tokio::sync::Mutex::new(std::time::Instant::now()) + }), + } + } + + /// Log a warning message if at least one hour has elapsed since the last warning + pub async fn warn_once_per_hour(&self, message: &str) { + let mut last = self.last_warning.lock().await; + if last.elapsed() >= Duration::from_secs(3600) { + *last = std::time::Instant::now(); + tracing::warn!("{message}"); + } + } +} + /// Return 200 OK if healthy. Used for readiness and liveness probes. #[route("/health", method = "GET", method = "HEAD")] async fn get_health() -> impl Responder { diff --git a/martin/src/srv/sprites.rs b/martin/src/srv/sprites.rs index 290e454b1..1cf278e72 100644 --- a/martin/src/srv/sprites.rs +++ b/martin/src/srv/sprites.rs @@ -1,6 +1,5 @@ use std::string::ToString; use std::sync::LazyLock; -use std::time::{Duration, Instant}; use actix_middleware_etag::Etag; use actix_web::error::ErrorNotFound; @@ -10,10 +9,8 @@ use actix_web::web::{Bytes, Data, Path}; use actix_web::{HttpResponse, Result as ActixResult, route}; use martin_core::sprites::{OptSpriteCache, SpriteError, SpriteSources}; use serde::Deserialize; -use tokio::sync::Mutex; -use tracing::warn; -use crate::srv::server::map_internal_error; +use crate::srv::server::{DebouncedWarning, map_internal_error}; #[derive(Deserialize)] pub struct SourceIDsRequest { @@ -49,17 +46,14 @@ async fn get_sprite_png( /// Redirect `/sprites/{source_ids}.png` to `/sprite/{source_ids}.png` (HTTP 301) #[route("/sprites/{source_ids}.png", method = "GET", method = "HEAD")] pub async fn redirect_sprites_png(path: Path) -> HttpResponse { + static WARNING: LazyLock = LazyLock::new(DebouncedWarning::new); let SourceIDsRequest { source_ids } = path.as_ref(); - static LAST_WARNING: LazyLock> = LazyLock::new(|| Mutex::new(Instant::now())); - - let mut warning = LAST_WARNING.lock().await; - if warning.elapsed() >= Duration::from_hours(1) { - *warning = Instant::now(); - warn!( - "Using /sprites/{source_ids}.png endpoint which causes an unnecessary redirect. Use /sprite/{source_ids}.png directly to avoid extra round-trip latency." - ); - } + WARNING + .warn_once_per_hour(&format!( + "Request to /sprites/{source_ids}.png caused unnecessary redirect. Use /sprite/{source_ids}.png to avoid extra round-trip latency." + )) + .await; HttpResponse::MovedPermanently() .insert_header((LOCATION, format!("/sprite/{source_ids}.png"))) @@ -95,17 +89,14 @@ async fn get_sprite_sdf_png( /// Redirect `/sdf_sprites/{source_ids}.png` to `/sdf_sprite/{source_ids}.png` (HTTP 301) #[route("/sdf_sprites/{source_ids}.png", method = "GET", method = "HEAD")] pub async fn redirect_sdf_sprites_png(path: Path) -> HttpResponse { + static WARNING: LazyLock = LazyLock::new(DebouncedWarning::new); let SourceIDsRequest { source_ids } = path.as_ref(); - static LAST_WARNING: LazyLock> = LazyLock::new(|| Mutex::new(Instant::now())); - - let mut warning = LAST_WARNING.lock().await; - if warning.elapsed() >= Duration::from_hours(1) { - *warning = Instant::now(); - warn!( - "Using /sdf_sprites/{source_ids}.png endpoint which causes an unnecessary redirect. Use /sdf_sprite/{source_ids}.png directly to avoid extra round-trip latency." - ); - } + WARNING + .warn_once_per_hour(&format!( + "Request to /sdf_sprites/{source_ids}.png caused unnecessary redirect. Use /sdf_sprite/{source_ids}.png to avoid extra round-trip latency." + )) + .await; HttpResponse::MovedPermanently() .insert_header((LOCATION, format!("/sdf_sprite/{source_ids}.png"))) @@ -142,17 +133,14 @@ async fn get_sprite_json( /// Redirect `/sprites/{source_ids}.json` to `/sprite/{source_ids}.json` (HTTP 301) #[route("/sprites/{source_ids}.json", method = "GET", method = "HEAD")] pub async fn redirect_sprites_json(path: Path) -> HttpResponse { + static WARNING: LazyLock = LazyLock::new(DebouncedWarning::new); let SourceIDsRequest { source_ids } = path.as_ref(); - static LAST_WARNING: LazyLock> = LazyLock::new(|| Mutex::new(Instant::now())); - - let mut warning = LAST_WARNING.lock().await; - if warning.elapsed() >= Duration::from_hours(1) { - *warning = Instant::now(); - warn!( - "Using /sprites/{source_ids}.json endpoint which causes an unnecessary redirect. Use /sprite/{source_ids}.json directly to avoid extra round-trip latency." - ); - } + WARNING + .warn_once_per_hour(&format!( + "Request to /sprites/{source_ids}.json caused unnecessary redirect. Use /sprite/{source_ids}.json to avoid extra round-trip latency." + )) + .await; HttpResponse::MovedPermanently() .insert_header((LOCATION, format!("/sprite/{source_ids}.json"))) @@ -189,17 +177,14 @@ async fn get_sprite_sdf_json( /// Redirect `/sdf_sprites/{source_ids}.json` to `/sdf_sprite/{source_ids}.json` (HTTP 301) #[route("/sdf_sprites/{source_ids}.json", method = "GET", method = "HEAD")] pub async fn redirect_sdf_sprites_json(path: Path) -> HttpResponse { + static WARNING: LazyLock = LazyLock::new(DebouncedWarning::new); let SourceIDsRequest { source_ids } = path.as_ref(); - static LAST_WARNING: LazyLock> = LazyLock::new(|| Mutex::new(Instant::now())); - - let mut warning = LAST_WARNING.lock().await; - if warning.elapsed() >= Duration::from_hours(1) { - *warning = Instant::now(); - warn!( - "Using /sdf_sprites/{source_ids}.json endpoint which causes an unnecessary redirect. Use /sdf_sprite/{source_ids}.json directly to avoid extra round-trip latency." - ); - } + WARNING + .warn_once_per_hour(&format!( + "Request to /sdf_sprites/{source_ids}.json caused unnecessary redirect. Use /sdf_sprite/{source_ids}.json to avoid extra round-trip latency." + )) + .await; HttpResponse::MovedPermanently() .insert_header((LOCATION, format!("/sdf_sprite/{source_ids}.json"))) diff --git a/martin/src/srv/styles.rs b/martin/src/srv/styles.rs index 8f8289018..74f342320 100644 --- a/martin/src/srv/styles.rs +++ b/martin/src/srv/styles.rs @@ -1,3 +1,5 @@ +use std::sync::LazyLock; + use actix_middleware_etag::Etag; use actix_web::http::header::{ContentType, LOCATION}; use actix_web::middleware::Compress; @@ -7,6 +9,8 @@ use martin_core::styles::StyleSources; use serde::Deserialize; use tracing::error; +use crate::srv::server::DebouncedWarning; + #[derive(Deserialize, Debug)] struct StyleRequest { style_id: String, @@ -53,15 +57,14 @@ async fn get_style_json(path: Path, styles: Data) -> /// This handles common pluralization mistakes #[route("/styles/{style_id}", method = "GET", method = "HEAD")] pub(crate) async fn redirect_styles(path: Path) -> HttpResponse { - static LAST_WARNING: LazyLock> = LazyLock::new(|| Mutex::new(Instant::now())); + static WARNING: LazyLock = LazyLock::new(DebouncedWarning::new); - let mut warning = LAST_WARNING.lock().await; - if warning.elapsed() >= Duration::from_hours(1) { - *warning = Instant::now(); - warn!( - "Using /fonts/{{fontstack}}/{{start}}-{{end}} endpoint which causes an unnecessary redirect. Use /font/{{fontstack}}/{{start}}-{{end}} directly to avoid extra round-trip latency." - ); - } + WARNING + .warn_once_per_hour(&format!( + "Request to /styles/{} caused unnecessary redirect. Use /style/{} to avoid extra round-trip latency.", + path.style_id, path.style_id + )) + .await; HttpResponse::MovedPermanently() .insert_header((LOCATION, format!("/style/{}", path.style_id))) diff --git a/martin/src/srv/tiles/content.rs b/martin/src/srv/tiles/content.rs index 1f6aca2e7..85b0f13e2 100644 --- a/martin/src/srv/tiles/content.rs +++ b/martin/src/srv/tiles/content.rs @@ -1,5 +1,4 @@ use std::sync::LazyLock; -use std::time::{Duration, Instant}; use actix_http::ContentEncoding; use actix_http::header::Quality; @@ -17,12 +16,11 @@ use martin_tile_utils::{ encode_gzip, }; use serde::Deserialize; -use tokio::sync::Mutex; -use tracing::warn; use crate::config::args::PreferredEncoding; use crate::config::file::srv::SrvConfig; use crate::source::TileSources; +use crate::srv::server::DebouncedWarning; use crate::srv::server::map_internal_error; const SUPPORTED_ENC: &[HeaderEnc] = &[ @@ -79,16 +77,14 @@ pub struct RedirectTileRequest { /// Registered before main tile route to match more specific pattern first #[route("/{ids}/{z}/{x}/{y}.{ext}", method = "GET", method = "HEAD")] pub async fn redirect_tile_ext(req: HttpRequest, path: Path) -> HttpResponse { + static WARNING: LazyLock = LazyLock::new(DebouncedWarning::new); let RedirectTileRequest { ids, z, x, y, ext } = path.as_ref(); - static LAST_WARNING: LazyLock> = LazyLock::new(|| Mutex::new(Instant::now())); - let mut warning = LAST_WARNING.lock().await; - if warning.elapsed() >= Duration::from_hours(1) { - *warning = Instant::now(); - warn!( - "Using /{ids}/{z}/{x}/{y}.{ext} endpoint which causes an unnecessary redirect. Use /{ids}/{z}/{x}/{y} directly to avoid extra round-trip latency.", - ); - } + WARNING + .warn_once_per_hour(&format!( + "Request to /{ids}/{z}/{x}/{y}.{ext} caused unnecessary redirect. Use /{ids}/{z}/{x}/{y} to avoid extra round-trip latency." + )) + .await; redirect_tile_with_query(ids, *z, *x, *y, req.query_string()) } @@ -96,21 +92,19 @@ pub async fn redirect_tile_ext(req: HttpRequest, path: Path /// Redirect `/tiles/{source_ids}/{z}/{x}/{y}` to `/{source_ids}/{z}/{x}/{y}` (HTTP 301) #[route("/tiles/{source_ids}/{z}/{x}/{y}", method = "GET", method = "HEAD")] pub async fn redirect_tiles(req: HttpRequest, path: Path) -> HttpResponse { + static WARNING: LazyLock = LazyLock::new(DebouncedWarning::new); let TileRequest { source_ids, z, x, y, } = path.as_ref(); - static LAST_WARNING: LazyLock> = LazyLock::new(|| Mutex::new(Instant::now())); - let mut warning = LAST_WARNING.lock().await; - if warning.elapsed() >= Duration::from_hours(1) { - *warning = Instant::now(); - warn!( - "Using /tiles/{source_ids}/{z}/{x}/{y} endpoint which causes an unnecessary redirect. Use /{source_ids}/{z}/{x}/{y} directly to avoid extra round-trip latency.", - ); - } + WARNING + .warn_once_per_hour(&format!( + "Request to /tiles/{source_ids}/{z}/{x}/{y} caused unnecessary redirect. Use /{source_ids}/{z}/{x}/{y} to avoid extra round-trip latency." + )) + .await; redirect_tile_with_query(source_ids, *z, *x, *y, req.query_string()) } From 58513e830504e88f44ed757d5d74b638b0c360c0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 00:49:03 +0000 Subject: [PATCH 25/37] Refactor DebouncedWarning API to use closure for better tracing integration - Changed API from `warn_once_per_hour(&str)` to `once_per_hour(|| warn!(...))` - This keeps the warn! call site in the caller's context for proper tracing log filtering - Updated all redirect handlers to use the new closure-based API - Added tracing::warn imports to all redirect modules Co-authored-by: CommanderStorm <26258709+CommanderStorm@users.noreply.github.com> --- martin/src/srv/fonts.rs | 11 +++++++---- martin/src/srv/server.rs | 8 +++++--- martin/src/srv/sprites.rs | 33 +++++++++++++++++++++------------ martin/src/srv/styles.rs | 12 +++++++----- martin/src/srv/tiles/content.rs | 17 +++++++++++------ 5 files changed, 51 insertions(+), 30 deletions(-) diff --git a/martin/src/srv/fonts.rs b/martin/src/srv/fonts.rs index d30cc5924..1f3b62ec8 100644 --- a/martin/src/srv/fonts.rs +++ b/martin/src/srv/fonts.rs @@ -9,6 +9,7 @@ use actix_web::web::{Data, Path}; use actix_web::{HttpResponse, Result as ActixResult, route}; use martin_core::fonts::{FontError, FontSources, OptFontCache}; use serde::Deserialize; +use tracing::warn; use crate::srv::server::{DebouncedWarning, map_internal_error}; @@ -51,10 +52,12 @@ pub async fn redirect_fonts(path: Path) -> HttpResponse { static WARNING: LazyLock = LazyLock::new(DebouncedWarning::new); WARNING - .warn_once_per_hour(&format!( - "Request to /fonts/{}/{}-{} caused unnecessary redirect. Use /font/{}/{}-{} to avoid extra round-trip latency.", - path.fontstack, path.start, path.end, path.fontstack, path.start, path.end - )) + .once_per_hour(|| { + warn!( + "Request to /fonts/{}/{}-{} caused unnecessary redirect. Use /font/{}/{}-{} to avoid extra round-trip latency.", + path.fontstack, path.start, path.end, path.fontstack, path.start, path.end + ); + }) .await; HttpResponse::MovedPermanently() diff --git a/martin/src/srv/server.rs b/martin/src/srv/server.rs index 3f6807408..bf7d2d021 100644 --- a/martin/src/srv/server.rs +++ b/martin/src/srv/server.rs @@ -51,12 +51,14 @@ impl DebouncedWarning { } } - /// Log a warning message if at least one hour has elapsed since the last warning - pub async fn warn_once_per_hour(&self, message: &str) { + /// Execute the provided closure at most once per hour. + /// This allows tracing's log filtering to work correctly by keeping the warn! call site + /// in the caller's context. + pub async fn once_per_hour(&self, f: F) { let mut last = self.last_warning.lock().await; if last.elapsed() >= Duration::from_secs(3600) { *last = std::time::Instant::now(); - tracing::warn!("{message}"); + f(); } } } diff --git a/martin/src/srv/sprites.rs b/martin/src/srv/sprites.rs index 1cf278e72..8c41b4aef 100644 --- a/martin/src/srv/sprites.rs +++ b/martin/src/srv/sprites.rs @@ -9,6 +9,7 @@ use actix_web::web::{Bytes, Data, Path}; use actix_web::{HttpResponse, Result as ActixResult, route}; use martin_core::sprites::{OptSpriteCache, SpriteError, SpriteSources}; use serde::Deserialize; +use tracing::warn; use crate::srv::server::{DebouncedWarning, map_internal_error}; @@ -50,9 +51,11 @@ pub async fn redirect_sprites_png(path: Path) -> HttpResponse let SourceIDsRequest { source_ids } = path.as_ref(); WARNING - .warn_once_per_hour(&format!( - "Request to /sprites/{source_ids}.png caused unnecessary redirect. Use /sprite/{source_ids}.png to avoid extra round-trip latency." - )) + .once_per_hour(|| { + warn!( + "Request to /sprites/{source_ids}.png caused unnecessary redirect. Use /sprite/{source_ids}.png to avoid extra round-trip latency." + ); + }) .await; HttpResponse::MovedPermanently() @@ -93,9 +96,11 @@ pub async fn redirect_sdf_sprites_png(path: Path) -> HttpRespo let SourceIDsRequest { source_ids } = path.as_ref(); WARNING - .warn_once_per_hour(&format!( - "Request to /sdf_sprites/{source_ids}.png caused unnecessary redirect. Use /sdf_sprite/{source_ids}.png to avoid extra round-trip latency." - )) + .once_per_hour(|| { + warn!( + "Request to /sdf_sprites/{source_ids}.png caused unnecessary redirect. Use /sdf_sprite/{source_ids}.png to avoid extra round-trip latency." + ); + }) .await; HttpResponse::MovedPermanently() @@ -137,9 +142,11 @@ pub async fn redirect_sprites_json(path: Path) -> HttpResponse let SourceIDsRequest { source_ids } = path.as_ref(); WARNING - .warn_once_per_hour(&format!( - "Request to /sprites/{source_ids}.json caused unnecessary redirect. Use /sprite/{source_ids}.json to avoid extra round-trip latency." - )) + .once_per_hour(|| { + warn!( + "Request to /sprites/{source_ids}.json caused unnecessary redirect. Use /sprite/{source_ids}.json to avoid extra round-trip latency." + ); + }) .await; HttpResponse::MovedPermanently() @@ -181,9 +188,11 @@ pub async fn redirect_sdf_sprites_json(path: Path) -> HttpResp let SourceIDsRequest { source_ids } = path.as_ref(); WARNING - .warn_once_per_hour(&format!( - "Request to /sdf_sprites/{source_ids}.json caused unnecessary redirect. Use /sdf_sprite/{source_ids}.json to avoid extra round-trip latency." - )) + .once_per_hour(|| { + warn!( + "Request to /sdf_sprites/{source_ids}.json caused unnecessary redirect. Use /sdf_sprite/{source_ids}.json to avoid extra round-trip latency." + ); + }) .await; HttpResponse::MovedPermanently() diff --git a/martin/src/srv/styles.rs b/martin/src/srv/styles.rs index 74f342320..8d67b7f0c 100644 --- a/martin/src/srv/styles.rs +++ b/martin/src/srv/styles.rs @@ -7,7 +7,7 @@ use actix_web::web::{Data, Path}; use actix_web::{HttpResponse, route}; use martin_core::styles::StyleSources; use serde::Deserialize; -use tracing::error; +use tracing::{error, warn}; use crate::srv::server::DebouncedWarning; @@ -60,10 +60,12 @@ pub(crate) async fn redirect_styles(path: Path) -> HttpResponse { static WARNING: LazyLock = LazyLock::new(DebouncedWarning::new); WARNING - .warn_once_per_hour(&format!( - "Request to /styles/{} caused unnecessary redirect. Use /style/{} to avoid extra round-trip latency.", - path.style_id, path.style_id - )) + .once_per_hour(|| { + warn!( + "Request to /styles/{} caused unnecessary redirect. Use /style/{} to avoid extra round-trip latency.", + path.style_id, path.style_id + ); + }) .await; HttpResponse::MovedPermanently() diff --git a/martin/src/srv/tiles/content.rs b/martin/src/srv/tiles/content.rs index 85b0f13e2..996da42b4 100644 --- a/martin/src/srv/tiles/content.rs +++ b/martin/src/srv/tiles/content.rs @@ -16,6 +16,7 @@ use martin_tile_utils::{ encode_gzip, }; use serde::Deserialize; +use tracing::warn; use crate::config::args::PreferredEncoding; use crate::config::file::srv::SrvConfig; @@ -81,9 +82,11 @@ pub async fn redirect_tile_ext(req: HttpRequest, path: Path let RedirectTileRequest { ids, z, x, y, ext } = path.as_ref(); WARNING - .warn_once_per_hour(&format!( - "Request to /{ids}/{z}/{x}/{y}.{ext} caused unnecessary redirect. Use /{ids}/{z}/{x}/{y} to avoid extra round-trip latency." - )) + .once_per_hour(|| { + warn!( + "Request to /{ids}/{z}/{x}/{y}.{ext} caused unnecessary redirect. Use /{ids}/{z}/{x}/{y} to avoid extra round-trip latency." + ); + }) .await; redirect_tile_with_query(ids, *z, *x, *y, req.query_string()) @@ -101,9 +104,11 @@ pub async fn redirect_tiles(req: HttpRequest, path: Path) -> HttpRe } = path.as_ref(); WARNING - .warn_once_per_hour(&format!( - "Request to /tiles/{source_ids}/{z}/{x}/{y} caused unnecessary redirect. Use /{source_ids}/{z}/{x}/{y} to avoid extra round-trip latency." - )) + .once_per_hour(|| { + warn!( + "Request to /tiles/{source_ids}/{z}/{x}/{y} caused unnecessary redirect. Use /{source_ids}/{z}/{x}/{y} to avoid extra round-trip latency." + ); + }) .await; redirect_tile_with_query(source_ids, *z, *x, *y, req.query_string()) From 0c9c79dacaa541089a1410fe11e7187c1d86c8b5 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Thu, 29 Jan 2026 03:01:53 +0100 Subject: [PATCH 26/37] Apply suggestions from code review --- martin/src/srv/sprites.rs | 8 ++++---- martin/src/srv/styles.rs | 7 +++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/martin/src/srv/sprites.rs b/martin/src/srv/sprites.rs index 8c41b4aef..5db74c8e4 100644 --- a/martin/src/srv/sprites.rs +++ b/martin/src/srv/sprites.rs @@ -47,7 +47,7 @@ async fn get_sprite_png( /// Redirect `/sprites/{source_ids}.png` to `/sprite/{source_ids}.png` (HTTP 301) #[route("/sprites/{source_ids}.png", method = "GET", method = "HEAD")] pub async fn redirect_sprites_png(path: Path) -> HttpResponse { - static WARNING: LazyLock = LazyLock::new(DebouncedWarning::new); + static WARNING: DebouncedWarning = DebouncedWarning::new(); let SourceIDsRequest { source_ids } = path.as_ref(); WARNING @@ -92,7 +92,7 @@ async fn get_sprite_sdf_png( /// Redirect `/sdf_sprites/{source_ids}.png` to `/sdf_sprite/{source_ids}.png` (HTTP 301) #[route("/sdf_sprites/{source_ids}.png", method = "GET", method = "HEAD")] pub async fn redirect_sdf_sprites_png(path: Path) -> HttpResponse { - static WARNING: LazyLock = LazyLock::new(DebouncedWarning::new); + static WARNING: DebouncedWarning = DebouncedWarning::new(); let SourceIDsRequest { source_ids } = path.as_ref(); WARNING @@ -138,7 +138,7 @@ async fn get_sprite_json( /// Redirect `/sprites/{source_ids}.json` to `/sprite/{source_ids}.json` (HTTP 301) #[route("/sprites/{source_ids}.json", method = "GET", method = "HEAD")] pub async fn redirect_sprites_json(path: Path) -> HttpResponse { - static WARNING: LazyLock = LazyLock::new(DebouncedWarning::new); + static WARNING: DebouncedWarning = DebouncedWarning::new(); let SourceIDsRequest { source_ids } = path.as_ref(); WARNING @@ -184,7 +184,7 @@ async fn get_sprite_sdf_json( /// Redirect `/sdf_sprites/{source_ids}.json` to `/sdf_sprite/{source_ids}.json` (HTTP 301) #[route("/sdf_sprites/{source_ids}.json", method = "GET", method = "HEAD")] pub async fn redirect_sdf_sprites_json(path: Path) -> HttpResponse { - static WARNING: LazyLock = LazyLock::new(DebouncedWarning::new); + static WARNING: DebouncedWarning = DebouncedWarning::new(); let SourceIDsRequest { source_ids } = path.as_ref(); WARNING diff --git a/martin/src/srv/styles.rs b/martin/src/srv/styles.rs index 8d67b7f0c..26ca4f203 100644 --- a/martin/src/srv/styles.rs +++ b/martin/src/srv/styles.rs @@ -57,18 +57,17 @@ async fn get_style_json(path: Path, styles: Data) -> /// This handles common pluralization mistakes #[route("/styles/{style_id}", method = "GET", method = "HEAD")] pub(crate) async fn redirect_styles(path: Path) -> HttpResponse { - static WARNING: LazyLock = LazyLock::new(DebouncedWarning::new); + static WARNING: DebouncedWarning = DebouncedWarning::new(); WARNING .once_per_hour(|| { warn!( - "Request to /styles/{} caused unnecessary redirect. Use /style/{} to avoid extra round-trip latency.", - path.style_id, path.style_id + "Request to /styles/{style_id} caused unnecessary redirect. Use /style/{style_id} to avoid extra round-trip latency." ); }) .await; HttpResponse::MovedPermanently() - .insert_header((LOCATION, format!("/style/{}", path.style_id))) + .insert_header((LOCATION, format!("/style/{style_id}"))) .finish() } From 3d60266385578dfb528bfeb4e324fe7b926c7c31 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Thu, 29 Jan 2026 03:02:16 +0100 Subject: [PATCH 27/37] Apply suggestion from @CommanderStorm --- martin/src/srv/tiles/content.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/martin/src/srv/tiles/content.rs b/martin/src/srv/tiles/content.rs index 996da42b4..2ba50140f 100644 --- a/martin/src/srv/tiles/content.rs +++ b/martin/src/srv/tiles/content.rs @@ -1,5 +1,3 @@ -use std::sync::LazyLock; - use actix_http::ContentEncoding; use actix_http::header::Quality; use actix_web::error::{ErrorBadRequest, ErrorNotAcceptable, ErrorNotFound}; From 7e5e34b360ae4f8543858e0b7a81ac131c3b3df4 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Thu, 29 Jan 2026 03:02:46 +0100 Subject: [PATCH 28/37] Apply suggestion from @CommanderStorm --- martin/src/srv/tiles/content.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/martin/src/srv/tiles/content.rs b/martin/src/srv/tiles/content.rs index 2ba50140f..7d106c34f 100644 --- a/martin/src/srv/tiles/content.rs +++ b/martin/src/srv/tiles/content.rs @@ -76,7 +76,7 @@ pub struct RedirectTileRequest { /// Registered before main tile route to match more specific pattern first #[route("/{ids}/{z}/{x}/{y}.{ext}", method = "GET", method = "HEAD")] pub async fn redirect_tile_ext(req: HttpRequest, path: Path) -> HttpResponse { - static WARNING: LazyLock = LazyLock::new(DebouncedWarning::new); + static WARNING: DebouncedWarning = DebouncedWarning::new(); let RedirectTileRequest { ids, z, x, y, ext } = path.as_ref(); WARNING From 5f65c700cc324d3a51040b8b69adeb1cb89509a9 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Thu, 29 Jan 2026 03:06:42 +0100 Subject: [PATCH 29/37] Apply suggestions from code review --- martin/src/srv/styles.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/martin/src/srv/styles.rs b/martin/src/srv/styles.rs index 26ca4f203..26bdd427f 100644 --- a/martin/src/srv/styles.rs +++ b/martin/src/srv/styles.rs @@ -58,7 +58,7 @@ async fn get_style_json(path: Path, styles: Data) -> #[route("/styles/{style_id}", method = "GET", method = "HEAD")] pub(crate) async fn redirect_styles(path: Path) -> HttpResponse { static WARNING: DebouncedWarning = DebouncedWarning::new(); - + let StyleRequest { style_id } = path.as_ref(); WARNING .once_per_hour(|| { warn!( From a4e5fbdb0945f277e15a402298df277e4ac67149 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Thu, 29 Jan 2026 03:07:22 +0100 Subject: [PATCH 30/37] Update martin/src/srv/tiles/content.rs --- martin/src/srv/tiles/content.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/martin/src/srv/tiles/content.rs b/martin/src/srv/tiles/content.rs index 7d106c34f..681e4fbdb 100644 --- a/martin/src/srv/tiles/content.rs +++ b/martin/src/srv/tiles/content.rs @@ -93,7 +93,7 @@ pub async fn redirect_tile_ext(req: HttpRequest, path: Path /// Redirect `/tiles/{source_ids}/{z}/{x}/{y}` to `/{source_ids}/{z}/{x}/{y}` (HTTP 301) #[route("/tiles/{source_ids}/{z}/{x}/{y}", method = "GET", method = "HEAD")] pub async fn redirect_tiles(req: HttpRequest, path: Path) -> HttpResponse { - static WARNING: LazyLock = LazyLock::new(DebouncedWarning::new); + static WARNING: DebouncedWarning = DebouncedWarning::new(); let TileRequest { source_ids, z, From 624d608bcbe3fb5db97a8e2dc85ca18ca9bfeac4 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Thu, 29 Jan 2026 03:07:33 +0100 Subject: [PATCH 31/37] Update martin/src/srv/fonts.rs --- martin/src/srv/fonts.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/martin/src/srv/fonts.rs b/martin/src/srv/fonts.rs index 1f3b62ec8..ea65e6352 100644 --- a/martin/src/srv/fonts.rs +++ b/martin/src/srv/fonts.rs @@ -49,7 +49,7 @@ async fn get_font( /// Redirect `/fonts/{fontstack}/{start}-{end}` to `/font/{fontstack}/{start}-{end}` (HTTP 301) #[route("/fonts/{fontstack}/{start}-{end}", method = "GET", method = "HEAD")] pub async fn redirect_fonts(path: Path) -> HttpResponse { - static WARNING: LazyLock = LazyLock::new(DebouncedWarning::new); + static WARNING: DebouncedWarning = DebouncedWarning::new(); WARNING .once_per_hour(|| { From 812475ce1a5b8db5117606b678d4ddfeaff95864 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Thu, 29 Jan 2026 03:07:51 +0100 Subject: [PATCH 32/37] Update martin/src/srv/fonts.rs --- martin/src/srv/fonts.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/martin/src/srv/fonts.rs b/martin/src/srv/fonts.rs index ea65e6352..c1fbce1a4 100644 --- a/martin/src/srv/fonts.rs +++ b/martin/src/srv/fonts.rs @@ -1,5 +1,4 @@ use std::string::ToString; -use std::sync::LazyLock; use actix_middleware_etag::Etag; use actix_web::error::{ErrorBadRequest, ErrorNotFound}; From 4c2392b4a88f0247e19e71bbc58a33ff82551112 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Thu, 29 Jan 2026 03:08:05 +0100 Subject: [PATCH 33/37] Update martin/src/srv/sprites.rs --- martin/src/srv/sprites.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/martin/src/srv/sprites.rs b/martin/src/srv/sprites.rs index 5db74c8e4..258a04a9e 100644 --- a/martin/src/srv/sprites.rs +++ b/martin/src/srv/sprites.rs @@ -1,5 +1,4 @@ use std::string::ToString; -use std::sync::LazyLock; use actix_middleware_etag::Etag; use actix_web::error::ErrorNotFound; From 3f181d3b8fca5422c8ebad077c8abad183f0c6ca Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Thu, 29 Jan 2026 03:08:25 +0100 Subject: [PATCH 34/37] Update martin/src/srv/styles.rs --- martin/src/srv/styles.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/martin/src/srv/styles.rs b/martin/src/srv/styles.rs index 26bdd427f..7aef90a04 100644 --- a/martin/src/srv/styles.rs +++ b/martin/src/srv/styles.rs @@ -1,5 +1,3 @@ -use std::sync::LazyLock; - use actix_middleware_etag::Etag; use actix_web::http::header::{ContentType, LOCATION}; use actix_web::middleware::Compress; From 82560d288579c7788116faf1689988fcaaa163f3 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Thu, 29 Jan 2026 03:26:19 +0100 Subject: [PATCH 35/37] Apply suggestion from @CommanderStorm --- martin/src/srv/server.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/martin/src/srv/server.rs b/martin/src/srv/server.rs index bf7d2d021..9f4a7dba0 100644 --- a/martin/src/srv/server.rs +++ b/martin/src/srv/server.rs @@ -35,12 +35,12 @@ pub fn map_internal_error(e: T) -> actix_web::Error { /// Helper struct for debounced warning messages in redirect handlers. /// Ensures warnings are logged no more than once per hour to avoid log spam. -#[cfg(any(feature = "_tiles", feature = "fonts", feature = "sprites"))] +#[cfg(any(feature = "_tiles", feature = "fonts", feature = "sprites", feature = "styles"))] pub struct DebouncedWarning { last_warning: std::sync::LazyLock>, } -#[cfg(any(feature = "_tiles", feature = "fonts", feature = "sprites"))] +#[cfg(any(feature = "_tiles", feature = "fonts", feature = "sprites", feature = "styles"))] impl DebouncedWarning { /// Create a new `DebouncedWarning` instance pub const fn new() -> Self { From e8d343f4a8588741916b194a63d0afbe2a2e1582 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 02:26:37 +0000 Subject: [PATCH 36/37] chore(fmt): apply pre-commit formatting fixes --- martin/src/srv/server.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/martin/src/srv/server.rs b/martin/src/srv/server.rs index 9f4a7dba0..f8d0d564b 100644 --- a/martin/src/srv/server.rs +++ b/martin/src/srv/server.rs @@ -35,12 +35,22 @@ pub fn map_internal_error(e: T) -> actix_web::Error { /// Helper struct for debounced warning messages in redirect handlers. /// Ensures warnings are logged no more than once per hour to avoid log spam. -#[cfg(any(feature = "_tiles", feature = "fonts", feature = "sprites", feature = "styles"))] +#[cfg(any( + feature = "_tiles", + feature = "fonts", + feature = "sprites", + feature = "styles" +))] pub struct DebouncedWarning { last_warning: std::sync::LazyLock>, } -#[cfg(any(feature = "_tiles", feature = "fonts", feature = "sprites", feature = "styles"))] +#[cfg(any( + feature = "_tiles", + feature = "fonts", + feature = "sprites", + feature = "styles" +))] impl DebouncedWarning { /// Create a new `DebouncedWarning` instance pub const fn new() -> Self { From 0eda4fae3394f9e6a51005be1d05b67013ed882e Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 02:38:21 +0000 Subject: [PATCH 37/37] chore: update blessed test snapshots across all components --- .../metrics_1.fetched_with_compression.txt | 60 +++++-------------- tests/expected/configured/metrics_1.txt | 60 +++++-------------- 2 files changed, 30 insertions(+), 90 deletions(-) diff --git a/tests/expected/configured/metrics_1.fetched_with_compression.txt b/tests/expected/configured/metrics_1.fetched_with_compression.txt index 56a6e4b43..43fa3b7fb 100644 --- a/tests/expected/configured/metrics_1.fetched_with_compression.txt +++ b/tests/expected/configured/metrics_1.fetched_with_compression.txt @@ -224,6 +224,20 @@ martin_http_requests_duration_seconds_bucket{endpoint="/tiles/{source_ids}/{z}/{ martin_http_requests_duration_seconds_bucket{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301",le="+Inf"} NUMBER martin_http_requests_duration_seconds_sum{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301"} NUMBER martin_http_requests_duration_seconds_count{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{ids}/{z}/{x}/{y}.{ext}",method="HEAD",status="301",le="0.005"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{ids}/{z}/{x}/{y}.{ext}",method="HEAD",status="301",le="0.01"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{ids}/{z}/{x}/{y}.{ext}",method="HEAD",status="301",le="0.025"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{ids}/{z}/{x}/{y}.{ext}",method="HEAD",status="301",le="0.05"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{ids}/{z}/{x}/{y}.{ext}",method="HEAD",status="301",le="0.1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{ids}/{z}/{x}/{y}.{ext}",method="HEAD",status="301",le="0.25"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{ids}/{z}/{x}/{y}.{ext}",method="HEAD",status="301",le="0.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{ids}/{z}/{x}/{y}.{ext}",method="HEAD",status="301",le="1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{ids}/{z}/{x}/{y}.{ext}",method="HEAD",status="301",le="2.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{ids}/{z}/{x}/{y}.{ext}",method="HEAD",status="301",le="5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{ids}/{z}/{x}/{y}.{ext}",method="HEAD",status="301",le="10"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{ids}/{z}/{x}/{y}.{ext}",method="HEAD",status="301",le="+Inf"} NUMBER +martin_http_requests_duration_seconds_sum{endpoint="/{ids}/{z}/{x}/{y}.{ext}",method="HEAD",status="301"} NUMBER +martin_http_requests_duration_seconds_count{endpoint="/{ids}/{z}/{x}/{y}.{ext}",method="HEAD",status="301"} NUMBER martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}",method="GET",status="200",le="0.005"} NUMBER martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}",method="GET",status="200",le="0.01"} NUMBER martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}",method="GET",status="200",le="0.025"} NUMBER @@ -252,48 +266,6 @@ martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y} martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}",method="GET",status="200",le="+Inf"} NUMBER martin_http_requests_duration_seconds_sum{endpoint="/{source_ids}/{z}/{x}/{y}",method="GET",status="200"} NUMBER martin_http_requests_duration_seconds_count{endpoint="/{source_ids}/{z}/{x}/{y}",method="GET",status="200"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="0.005"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="0.01"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="0.025"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="0.05"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="0.1"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="0.25"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="0.5"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="1"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="2.5"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="5"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="10"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="+Inf"} NUMBER -martin_http_requests_duration_seconds_sum{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301"} NUMBER -martin_http_requests_duration_seconds_count{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="0.005"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="0.01"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="0.025"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="0.05"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="0.1"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="0.25"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="0.5"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="1"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="2.5"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="5"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="10"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="+Inf"} NUMBER -martin_http_requests_duration_seconds_sum{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301"} NUMBER -martin_http_requests_duration_seconds_count{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="0.005"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="0.01"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="0.025"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="0.05"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="0.1"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="0.25"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="0.5"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="1"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="2.5"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="5"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="10"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="+Inf"} NUMBER -martin_http_requests_duration_seconds_sum{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301"} NUMBER -martin_http_requests_duration_seconds_count{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301"} NUMBER # HELP martin_http_requests_total Total number of HTTP requests # TYPE martin_http_requests_total counter martin_http_requests_total{endpoint="/_/metrics",method="GET",status="200"} NUMBER @@ -312,8 +284,6 @@ martin_http_requests_total{endpoint="/sprites/{source_ids}.png",method="HEAD",st martin_http_requests_total{endpoint="/style/{style_id}",method="GET",status="200"} NUMBER martin_http_requests_total{endpoint="/styles/{style_id}",method="HEAD",status="301"} NUMBER martin_http_requests_total{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301"} NUMBER +martin_http_requests_total{endpoint="/{ids}/{z}/{x}/{y}.{ext}",method="HEAD",status="301"} NUMBER martin_http_requests_total{endpoint="/{source_ids}",method="GET",status="200"} NUMBER martin_http_requests_total{endpoint="/{source_ids}/{z}/{x}/{y}",method="GET",status="200"} NUMBER -martin_http_requests_total{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301"} NUMBER -martin_http_requests_total{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301"} NUMBER -martin_http_requests_total{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301"} NUMBER diff --git a/tests/expected/configured/metrics_1.txt b/tests/expected/configured/metrics_1.txt index 9f55b4016..af259e02e 100644 --- a/tests/expected/configured/metrics_1.txt +++ b/tests/expected/configured/metrics_1.txt @@ -210,6 +210,20 @@ martin_http_requests_duration_seconds_bucket{endpoint="/tiles/{source_ids}/{z}/{ martin_http_requests_duration_seconds_bucket{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301",le="+Inf"} NUMBER martin_http_requests_duration_seconds_sum{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301"} NUMBER martin_http_requests_duration_seconds_count{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{ids}/{z}/{x}/{y}.{ext}",method="HEAD",status="301",le="0.005"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{ids}/{z}/{x}/{y}.{ext}",method="HEAD",status="301",le="0.01"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{ids}/{z}/{x}/{y}.{ext}",method="HEAD",status="301",le="0.025"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{ids}/{z}/{x}/{y}.{ext}",method="HEAD",status="301",le="0.05"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{ids}/{z}/{x}/{y}.{ext}",method="HEAD",status="301",le="0.1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{ids}/{z}/{x}/{y}.{ext}",method="HEAD",status="301",le="0.25"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{ids}/{z}/{x}/{y}.{ext}",method="HEAD",status="301",le="0.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{ids}/{z}/{x}/{y}.{ext}",method="HEAD",status="301",le="1"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{ids}/{z}/{x}/{y}.{ext}",method="HEAD",status="301",le="2.5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{ids}/{z}/{x}/{y}.{ext}",method="HEAD",status="301",le="5"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{ids}/{z}/{x}/{y}.{ext}",method="HEAD",status="301",le="10"} NUMBER +martin_http_requests_duration_seconds_bucket{endpoint="/{ids}/{z}/{x}/{y}.{ext}",method="HEAD",status="301",le="+Inf"} NUMBER +martin_http_requests_duration_seconds_sum{endpoint="/{ids}/{z}/{x}/{y}.{ext}",method="HEAD",status="301"} NUMBER +martin_http_requests_duration_seconds_count{endpoint="/{ids}/{z}/{x}/{y}.{ext}",method="HEAD",status="301"} NUMBER martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}",method="GET",status="200",le="0.005"} NUMBER martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}",method="GET",status="200",le="0.01"} NUMBER martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}",method="GET",status="200",le="0.025"} NUMBER @@ -238,48 +252,6 @@ martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y} martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}",method="GET",status="200",le="+Inf"} NUMBER martin_http_requests_duration_seconds_sum{endpoint="/{source_ids}/{z}/{x}/{y}",method="GET",status="200"} NUMBER martin_http_requests_duration_seconds_count{endpoint="/{source_ids}/{z}/{x}/{y}",method="GET",status="200"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="0.005"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="0.01"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="0.025"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="0.05"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="0.1"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="0.25"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="0.5"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="1"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="2.5"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="5"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="10"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301",le="+Inf"} NUMBER -martin_http_requests_duration_seconds_sum{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301"} NUMBER -martin_http_requests_duration_seconds_count{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="0.005"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="0.01"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="0.025"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="0.05"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="0.1"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="0.25"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="0.5"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="1"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="2.5"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="5"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="10"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301",le="+Inf"} NUMBER -martin_http_requests_duration_seconds_sum{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301"} NUMBER -martin_http_requests_duration_seconds_count{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="0.005"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="0.01"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="0.025"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="0.05"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="0.1"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="0.25"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="0.5"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="1"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="2.5"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="5"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="10"} NUMBER -martin_http_requests_duration_seconds_bucket{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301",le="+Inf"} NUMBER -martin_http_requests_duration_seconds_sum{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301"} NUMBER -martin_http_requests_duration_seconds_count{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301"} NUMBER # HELP martin_http_requests_total Total number of HTTP requests # TYPE martin_http_requests_total counter martin_http_requests_total{endpoint="/catalog",method="GET",status="200"} NUMBER @@ -297,8 +269,6 @@ martin_http_requests_total{endpoint="/sprites/{source_ids}.png",method="HEAD",st martin_http_requests_total{endpoint="/style/{style_id}",method="GET",status="200"} NUMBER martin_http_requests_total{endpoint="/styles/{style_id}",method="HEAD",status="301"} NUMBER martin_http_requests_total{endpoint="/tiles/{source_ids}/{z}/{x}/{y}",method="HEAD",status="301"} NUMBER +martin_http_requests_total{endpoint="/{ids}/{z}/{x}/{y}.{ext}",method="HEAD",status="301"} NUMBER martin_http_requests_total{endpoint="/{source_ids}",method="GET",status="200"} NUMBER martin_http_requests_total{endpoint="/{source_ids}/{z}/{x}/{y}",method="GET",status="200"} NUMBER -martin_http_requests_total{endpoint="/{source_ids}/{z}/{x}/{y}.mlt",method="HEAD",status="301"} NUMBER -martin_http_requests_total{endpoint="/{source_ids}/{z}/{x}/{y}.mvt",method="HEAD",status="301"} NUMBER -martin_http_requests_total{endpoint="/{source_ids}/{z}/{x}/{y}.pbf",method="HEAD",status="301"} NUMBER