From 2086293077571bf991e737e2b6326793ff1f0643 Mon Sep 17 00:00:00 2001 From: jasonyess Date: Wed, 1 Apr 2026 20:09:54 -0400 Subject: [PATCH] the fairing --- pointercrate-core-api/src/lib.rs | 1 + pointercrate-core-api/src/normalize_uri.rs | 66 ++++++++++++++++++++++ pointercrate-example/src/main.rs | 2 + 3 files changed, 69 insertions(+) create mode 100644 pointercrate-core-api/src/normalize_uri.rs diff --git a/pointercrate-core-api/src/lib.rs b/pointercrate-core-api/src/lib.rs index 853f074c1..0a02280e0 100644 --- a/pointercrate-core-api/src/lib.rs +++ b/pointercrate-core-api/src/lib.rs @@ -2,6 +2,7 @@ pub mod error; pub mod etag; pub mod localization; pub mod maintenance; +pub mod normalize_uri; pub mod pagination; pub mod preferences; pub mod query; diff --git a/pointercrate-core-api/src/normalize_uri.rs b/pointercrate-core-api/src/normalize_uri.rs new file mode 100644 index 000000000..d545dc909 --- /dev/null +++ b/pointercrate-core-api/src/normalize_uri.rs @@ -0,0 +1,66 @@ +use std::sync::OnceLock; + +use rocket::{ + fairing::{Fairing, Info, Kind}, + Data, Orbit, Request, Rocket, Route, +}; + +// heavily inspired by rocket's `rocket::fairing::AdHoc::uri_normalizer()` implementation +// only difference is that this applies a trailing slash internally as opposed to omitting it +// https://api.rocket.rs/master/src/rocket/fairing/ad_hoc.rs#315 +pub fn uri_normalizer() -> impl Fairing { + #[derive(Default)] + struct Normalizer { + routes: OnceLock>, + } + + impl Normalizer { + fn routes(&self, rocket: &Rocket) -> &[Route] { + // gather all defined routes which have a trailing slash + self.routes.get_or_init(|| { + rocket + .routes() + .filter(|r| r.uri.has_trailing_slash() || r.uri.path() == "/") + .cloned() + .collect() + }) + } + } + + #[rocket::async_trait] + impl Fairing for Normalizer { + fn info(&self) -> Info { + Info { + name: "URI Normalizer", + kind: Kind::Liftoff | Kind::Request, + } + } + + async fn on_liftoff(&self, rocket: &Rocket) { + let _ = self.routes(rocket); + } + + async fn on_request(&self, request: &mut Request<'_>, _: &mut Data<'_>) { + if request.uri().has_trailing_slash() { + return; + } + + if let Some(normalized) = request.uri().map_path(|p| format!("{}/", p)) { + // check if the normalized uri (the request uri with a trailing slash) matches one of our defined routes + let mut normalized_req = request.clone(); + normalized_req.set_uri(normalized.clone()); + + if self.routes(request.rocket()).iter().any(|r| { + // we need to leverage rocket's route matching otherwise this will suck + r.matches(&normalized_req) + }) { + // the request doesn't have a trailing slash AND it's trying to reach one of our defined routes + // so just point it to our defined route + request.set_uri(normalized); + } + } + } + } + + Normalizer::default() +} diff --git a/pointercrate-example/src/main.rs b/pointercrate-example/src/main.rs index 035c5240e..c5b5ab385 100644 --- a/pointercrate-example/src/main.rs +++ b/pointercrate-example/src/main.rs @@ -2,6 +2,7 @@ use maud::html; use pointercrate_core::localization::LocalesLoader; use pointercrate_core::pool::PointercratePool; use pointercrate_core::{error::CoreError, localization::tr}; +use pointercrate_core_api::normalize_uri::uri_normalizer; use pointercrate_core_api::{error::ErrorResponder, maintenance::MaintenanceFairing, preferences::PreferenceManager}; use pointercrate_core_macros::localized_catcher; use pointercrate_core_pages::{ @@ -178,6 +179,7 @@ async fn rocket() -> _ { // static files. rocket + .attach(uri_normalizer()) .mount("/static/core", FileServer::new("pointercrate-core-pages/static")) .mount("/static/demonlist", FileServer::new("pointercrate-demonlist-pages/static")) .mount("/static/user", FileServer::new("pointercrate-user-pages/static"))