Skip to main content

tachyon_web/routing/
mod.rs

1//! Axum-compatible Routing and Handler traits.
2//!
3//! The routing module provides the core mechanism for matching incoming HTTP requests to
4//! asynchronous handlers. It embraces an **Axum-like API**, allowing you to build highly
5//! readable and modular applications using `Router` and `MethodRouter`.
6//!
7//! By chaining methods like `.route("/", get(handler))` and `.with_state(state)`, you can
8//! effortlessly create scalable web endpoints that automatically extract parameters, state,
9//! and payloads with zero-allocation abstractions.
10
11use bytes::Bytes;
12use hyper::{Method, Request, Response, StatusCode};
13use std::future::Future;
14use std::sync::Arc;
15
16pub mod extract;
17pub mod handler;
18/// Middleware primitives like the `Next` continuation struct.
19pub mod middleware;
20/// Module for serving static directories with compile-time-like performance.
21pub mod static_dir;
22/// Optional `tower::Service`/`tower::Layer` interop, gated behind the `tower` feature.
23#[cfg(feature = "tower")]
24pub mod tower_compat;
25
26use crate::http::response::{Body, IntoResponse};
27use crate::routing::extract::PathParams;
28pub use handler::{BoxedFuture, BoxedHandler, Handler};
29
30// ─── Method array indices ─────────────────────────────────────────────────────
31
32const IDX_GET: usize = 0;
33const IDX_POST: usize = 1;
34const IDX_PUT: usize = 2;
35const IDX_DELETE: usize = 3;
36const IDX_OPTIONS: usize = 4;
37const IDX_HEAD: usize = 5;
38const IDX_PATCH: usize = 6;
39const IDX_TRACE: usize = 7;
40const IDX_CONNECT: usize = 8;
41const METHOD_COUNT: usize = 9;
42
43const METHOD_NAMES: [&str; METHOD_COUNT] = [
44    "GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE", "CONNECT",
45];
46
47#[inline]
48const fn method_index(m: &Method) -> Option<usize> {
49    match *m {
50        Method::GET => Some(IDX_GET),
51        Method::POST => Some(IDX_POST),
52        Method::PUT => Some(IDX_PUT),
53        Method::DELETE => Some(IDX_DELETE),
54        Method::OPTIONS => Some(IDX_OPTIONS),
55        Method::HEAD => Some(IDX_HEAD),
56        Method::PATCH => Some(IDX_PATCH),
57        Method::TRACE => Some(IDX_TRACE),
58        Method::CONNECT => Some(IDX_CONNECT),
59        _ => None,
60    }
61}
62
63// ─── MethodRouter ─────────────────────────────────────────────────────────────
64
65/// Router that dispatches requests to different handlers based on the HTTP method.
66#[derive(Clone)]
67pub struct MethodRouter<S> {
68    handlers: [Option<middleware::MethodHandler<S>>; METHOD_COUNT],
69    /// Path-parameter names for this route, in declaration order, populated by
70    /// `Router::compile()`. Cloning an `Arc<str>` per request into `PathParams`
71    /// is a refcount bump, avoiding a fresh heap allocation for every param name
72    /// on every request (the names are fixed once the route is compiled).
73    param_names: Arc<[Arc<str>]>,
74    /// The compiled route pattern this handler is registered under (e.g.
75    /// `/users/{id}`), populated by `Router::compile()`. Exposed to handlers via
76    /// the [`crate::routing::extract::MatchedPath`] extractor, matching Axum.
77    matched_path: Arc<str>,
78    /// When this route was reached through one or more `Router::nest()` calls,
79    /// the accumulated prefix to strip from the request `Uri` before dispatch —
80    /// matching Axum's nested-router URI rewriting (see `Router::nest`).
81    nest_prefix: Option<Arc<str>>,
82}
83
84impl<S> std::fmt::Debug for MethodRouter<S> {
85    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        let methods = [
87            "GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE", "CONNECT",
88        ];
89        let mut dbg = f.debug_struct("MethodRouter");
90        for (i, name) in methods.iter().enumerate() {
91            let _ = dbg.field(name, &self.handlers[i].is_some());
92        }
93        let _ = dbg.field("param_names", &self.param_names);
94        let _ = dbg.field("matched_path", &self.matched_path);
95        let _ = dbg.field("nest_prefix", &self.nest_prefix);
96        dbg.finish()
97    }
98}
99
100impl<S> Default for MethodRouter<S>
101where
102    S: Clone + Send + Sync + 'static,
103{
104    fn default() -> Self {
105        Self::new()
106    }
107}
108
109impl<S> MethodRouter<S>
110where
111    S: Clone + Send + Sync + 'static,
112{
113    /// Create a new empty `MethodRouter`.
114    #[must_use]
115    pub fn new() -> Self {
116        Self {
117            handlers: [None, None, None, None, None, None, None, None, None],
118            param_names: Arc::from([]),
119            matched_path: Arc::from(""),
120            nest_prefix: None,
121        }
122    }
123
124    fn set<H, T>(mut self, idx: usize, handler: H) -> Self
125    where
126        H: Handler<T, S>,
127        T: 'static,
128    {
129        self.handlers[idx] = Some(middleware::MethodHandler::new(Arc::new(
130            move |req, state| handler.clone().call(req, state),
131        )));
132        self
133    }
134
135    /// Add a handler for HTTP GET requests.
136    #[must_use]
137    pub fn get<H, T>(self, handler: H) -> Self
138    where
139        H: Handler<T, S>,
140        T: 'static,
141    {
142        self.set(IDX_GET, handler)
143    }
144    /// Add a handler for HTTP POST requests.
145    #[must_use]
146    pub fn post<H, T>(self, handler: H) -> Self
147    where
148        H: Handler<T, S>,
149        T: 'static,
150    {
151        self.set(IDX_POST, handler)
152    }
153    /// Add a handler for HTTP PUT requests.
154    #[must_use]
155    pub fn put<H, T>(self, handler: H) -> Self
156    where
157        H: Handler<T, S>,
158        T: 'static,
159    {
160        self.set(IDX_PUT, handler)
161    }
162    /// Add a handler for HTTP DELETE requests.
163    #[must_use]
164    pub fn delete<H, T>(self, handler: H) -> Self
165    where
166        H: Handler<T, S>,
167        T: 'static,
168    {
169        self.set(IDX_DELETE, handler)
170    }
171    /// Add a handler for HTTP OPTIONS requests.
172    #[must_use]
173    pub fn options<H, T>(self, handler: H) -> Self
174    where
175        H: Handler<T, S>,
176        T: 'static,
177    {
178        self.set(IDX_OPTIONS, handler)
179    }
180    /// Add a handler for HTTP HEAD requests.
181    #[must_use]
182    pub fn head<H, T>(self, handler: H) -> Self
183    where
184        H: Handler<T, S>,
185        T: 'static,
186    {
187        self.set(IDX_HEAD, handler)
188    }
189    /// Add a handler for HTTP PATCH requests.
190    #[must_use]
191    pub fn patch<H, T>(self, handler: H) -> Self
192    where
193        H: Handler<T, S>,
194        T: 'static,
195    {
196        self.set(IDX_PATCH, handler)
197    }
198    /// Add a handler for HTTP TRACE requests.
199    #[must_use]
200    pub fn trace<H, T>(self, handler: H) -> Self
201    where
202        H: Handler<T, S>,
203        T: 'static,
204    {
205        self.set(IDX_TRACE, handler)
206    }
207    /// Add a handler for HTTP CONNECT requests.
208    #[must_use]
209    pub fn connect<H, T>(self, handler: H) -> Self
210    where
211        H: Handler<T, S>,
212        T: 'static,
213    {
214        self.set(IDX_CONNECT, handler)
215    }
216
217    /// Returns the `Allow` header value listing all registered HTTP methods.
218    ///
219    /// Builds the string with a single pre-sized allocation (sized for the
220    /// worst case: all nine methods plus a synthesized `HEAD`) and avoids
221    /// an intermediate `Vec` — called only on 405 responses.
222    ///
223    /// Matches Axum's format: comma-joined with no space, and — since a `GET`
224    /// handler transparently answers `HEAD` requests too when no explicit
225    /// `HEAD` handler is registered (see `handle_request`'s
226    /// `falls_back_to_get` logic) — `HEAD` is listed whenever `GET` is
227    /// registered, even without an explicit `HEAD` handler.
228    fn allow_header(&self) -> String {
229        let mut out = String::with_capacity(56);
230        let implicit_head = self.handlers[IDX_GET].is_some() && self.handlers[IDX_HEAD].is_none();
231        for (i, name) in METHOD_NAMES.iter().enumerate() {
232            if self.handlers[i].is_some() {
233                if !out.is_empty() {
234                    out.push(',');
235                }
236                out.push_str(name);
237                if i == IDX_GET && implicit_head {
238                    out.push_str(",HEAD");
239                }
240            }
241        }
242        out
243    }
244
245    /// Merges `other`'s method handlers into `self`, matching Axum's
246    /// `Router::route` semantics where registering the same path twice with
247    /// non-overlapping methods combines into a single route — e.g.
248    /// `.route("/x", get(a)).route("/x", post(b))` yields one route that
249    /// answers both `GET` and `POST` on `/x`, rather than tachyon-web's
250    /// previous behavior of rejecting *any* repeated path outright.
251    ///
252    /// # Errors
253    /// Returns [`RouterError::MethodOverlap`] if `other` defines a method
254    /// already present in `self` — matching Axum, which panics with
255    /// "Overlapping method route" for the same situation.
256    fn merge(mut self, mut other: Self, path: &str) -> Result<Self, RouterError> {
257        for (i, (mine, theirs)) in self
258            .handlers
259            .iter_mut()
260            .zip(other.handlers.iter_mut())
261            .enumerate()
262        {
263            if let Some(handler) = theirs.take() {
264                if mine.is_some() {
265                    return Err(RouterError::MethodOverlap {
266                        method: METHOD_NAMES[i],
267                        path: path.to_string(),
268                    });
269                }
270                *mine = Some(handler);
271            }
272        }
273        // `nest_prefix` isn't recomputed at `compile()` time (unlike
274        // `param_names`/`matched_path`), so a prefix carried by either side
275        // must survive the merge — otherwise a route registered via both
276        // `.nest()` and a plain `.route()` at the same final path would
277        // silently lose its prefix-stripping behavior depending on
278        // registration order.
279        if self.nest_prefix.is_none() {
280            self.nest_prefix = other.nest_prefix;
281        }
282        Ok(self)
283    }
284
285    /// Apply a middleware handler to all endpoints registered in this `MethodRouter`.
286    ///
287    /// Middleware takes a `Request` and a `Next<S>` continuation.
288    #[must_use]
289    pub fn hoop<F, Fut, Res>(self, middleware: F) -> Self
290    where
291        F: Fn(Request<Body>, middleware::Next<S>) -> Fut + Clone + Send + Sync + 'static,
292        Fut: Future<Output = Res> + Send + 'static,
293        Res: IntoResponse + Send + 'static,
294    {
295        self.hoop_at(middleware::MiddlewarePosition::First, middleware)
296    }
297
298    /// Apply a middleware handler to all endpoints registered in this `MethodRouter` at a specific position (First/Last).
299    #[must_use]
300    pub fn hoop_at<F, Fut, Res>(
301        mut self,
302        position: middleware::MiddlewarePosition,
303        middleware: F,
304    ) -> Self
305    where
306        F: Fn(Request<Body>, middleware::Next<S>) -> Fut + Clone + Send + Sync + 'static,
307        Fut: Future<Output = Res> + Send + 'static,
308        Res: IntoResponse + Send + 'static,
309    {
310        let boxed: middleware::BoxedMiddleware<S> = Arc::new(move |req, next| {
311            let fut = middleware(req, next);
312            crate::routing::handler::ResponseFuture::Boxed(Box::pin(async move {
313                fut.await.into_response()
314            }))
315        });
316        for i in 0..METHOD_COUNT {
317            if let Some(handler) = &mut self.handlers[i] {
318                match position {
319                    middleware::MiddlewarePosition::First => {
320                        handler.middlewares.insert(0, boxed.clone());
321                    }
322                    middleware::MiddlewarePosition::Last => {
323                        handler.middlewares.push(boxed.clone());
324                    }
325                }
326                handler.compiled = None;
327            }
328        }
329        self
330    }
331
332    /// Compile all handler middleware chains in-place.
333    pub fn compile_in_place(&mut self) {
334        for i in 0..METHOD_COUNT {
335            if let Some(handler) = &mut self.handlers[i] {
336                handler.compile_in_place();
337            }
338        }
339    }
340
341    /// Capture a state and transition this method router to another state type.
342    #[must_use]
343    pub fn with_state<S2>(self, state: &Arc<S>) -> MethodRouter<S2>
344    where
345        S2: Clone + Send + Sync + 'static,
346        S: Clone + Send + Sync + 'static,
347    {
348        let mut new_handlers: [Option<middleware::MethodHandler<S2>>; METHOD_COUNT] =
349            [None, None, None, None, None, None, None, None, None];
350        for (i, opt_handler) in self.handlers.iter().enumerate() {
351            if let Some(handler) = opt_handler {
352                let mut compiled_h = handler.clone();
353                compiled_h.compile_in_place();
354                let compiled_raw = compiled_h.compiled.unwrap_or(compiled_h.raw);
355                let state = state.clone();
356                let new_h: BoxedHandler<S2> =
357                    Arc::new(move |req, _parent_state| compiled_raw(req, state.clone()));
358                new_handlers[i] = Some(middleware::MethodHandler::new(new_h));
359            }
360        }
361        MethodRouter {
362            handlers: new_handlers,
363            param_names: self.param_names,
364            matched_path: self.matched_path,
365            nest_prefix: self.nest_prefix,
366        }
367    }
368}
369
370// ─── MethodRouter shortcuts ───────────────────────────────────────────────────
371
372/// Helper to construct a GET-only route.
373pub fn get<H, T, S>(handler: H) -> MethodRouter<S>
374where
375    H: Handler<T, S>,
376    T: 'static,
377    S: Clone + Send + Sync + 'static,
378{
379    MethodRouter::new().get(handler)
380}
381
382/// Helper to construct a POST-only route.
383pub fn post<H, T, S>(handler: H) -> MethodRouter<S>
384where
385    H: Handler<T, S>,
386    T: 'static,
387    S: Clone + Send + Sync + 'static,
388{
389    MethodRouter::new().post(handler)
390}
391
392/// Helper to construct a PUT-only route.
393pub fn put<H, T, S>(handler: H) -> MethodRouter<S>
394where
395    H: Handler<T, S>,
396    T: 'static,
397    S: Clone + Send + Sync + 'static,
398{
399    MethodRouter::new().put(handler)
400}
401
402/// Helper to construct a DELETE-only route.
403pub fn delete<H, T, S>(handler: H) -> MethodRouter<S>
404where
405    H: Handler<T, S>,
406    T: 'static,
407    S: Clone + Send + Sync + 'static,
408{
409    MethodRouter::new().delete(handler)
410}
411
412/// Helper to construct a PATCH-only route.
413pub fn patch<H, T, S>(handler: H) -> MethodRouter<S>
414where
415    H: Handler<T, S>,
416    T: 'static,
417    S: Clone + Send + Sync + 'static,
418{
419    MethodRouter::new().patch(handler)
420}
421
422/// Helper to construct an OPTIONS-only route.
423pub fn options<H, T, S>(handler: H) -> MethodRouter<S>
424where
425    H: Handler<T, S>,
426    T: 'static,
427    S: Clone + Send + Sync + 'static,
428{
429    MethodRouter::new().options(handler)
430}
431
432/// Helper to construct a HEAD-only route.
433pub fn head<H, T, S>(handler: H) -> MethodRouter<S>
434where
435    H: Handler<T, S>,
436    T: 'static,
437    S: Clone + Send + Sync + 'static,
438{
439    MethodRouter::new().head(handler)
440}
441
442/// Helper to construct a TRACE-only route.
443pub fn trace<H, T, S>(handler: H) -> MethodRouter<S>
444where
445    H: Handler<T, S>,
446    T: 'static,
447    S: Clone + Send + Sync + 'static,
448{
449    MethodRouter::new().trace(handler)
450}
451
452/// Helper to construct a CONNECT-only route.
453pub fn connect<H, T, S>(handler: H) -> MethodRouter<S>
454where
455    H: Handler<T, S>,
456    T: 'static,
457    S: Clone + Send + Sync + 'static,
458{
459    MethodRouter::new().connect(handler)
460}
461
462/// Helper to construct a route that dispatches to `handler` for **every** HTTP
463/// method (`GET`, `POST`, `PUT`, `DELETE`, `OPTIONS`, `HEAD`, `PATCH`, `TRACE`,
464/// `CONNECT`). Matches `axum::routing::any`.
465pub fn any<H, T, S>(handler: H) -> MethodRouter<S>
466where
467    H: Handler<T, S>,
468    T: 'static,
469    S: Clone + Send + Sync + 'static,
470{
471    MethodRouter::new()
472        .get(handler.clone())
473        .post(handler.clone())
474        .put(handler.clone())
475        .delete(handler.clone())
476        .options(handler.clone())
477        .head(handler.clone())
478        .patch(handler.clone())
479        .trace(handler.clone())
480        .connect(handler)
481}
482
483// ─── Router ──────────────────────────────────────────────────────────────────
484
485/// Axum-like routing table using `matchit` under the hood.
486#[derive(Clone)]
487pub struct Router<S = ()> {
488    routes: Vec<(String, MethodRouter<S>)>,
489    fallback: Option<BoxedHandler<S>>,
490    method_not_allowed_fallback: Option<BoxedHandler<S>>,
491    state: Option<Arc<S>>,
492    /// Opt-in trailing-slash normalization (see [`Router::normalize_trailing_slash`]).
493    normalize_trailing_slash: bool,
494    /// Lazily populated the first time this exact `Router` is driven as a
495    /// `tower::Service` (see the `impl Service<...> for Router<S>` below) —
496    /// lets `Router` be used as a drop-in `tower::Service`/`.oneshot()`
497    /// target exactly like `axum::Router`, with no separate `.compile()`
498    /// call the caller has to remember, while still only ever building the
499    /// `matchit` tree once rather than per request. Reset to `None` by every
500    /// route-table-mutating builder method, so building/serving/mutating
501    /// out of order can't silently dispatch against a stale tree.
502    #[cfg(feature = "tower")]
503    compiled: Option<CompiledRouter<S>>,
504}
505
506impl<S> std::fmt::Debug for Router<S> {
507    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
508        f.debug_struct("Router")
509            .field("route_count", &self.routes.len())
510            .field("has_fallback", &self.fallback.is_some())
511            .field(
512                "has_method_not_allowed_fallback",
513                &self.method_not_allowed_fallback.is_some(),
514            )
515            .finish_non_exhaustive()
516    }
517}
518
519impl<S> Default for Router<S>
520where
521    S: Clone + Send + Sync + 'static,
522{
523    fn default() -> Self {
524        Self::new()
525    }
526}
527
528/// Combines two optional per-router handlers (a `fallback` or
529/// `method_not_allowed_fallback`) for [`Router::merge`]: `None` if neither side set one,
530/// whichever side's if only one did, or a panic with `panic_msg` if both did — matching
531/// Axum's "Cannot merge two `Router`s that both have a fallback".
532#[allow(clippy::panic)]
533fn merge_optional<T>(a: Option<T>, b: Option<T>, panic_msg: &'static str) -> Option<T> {
534    match (a, b) {
535        (Some(_), Some(_)) => panic!("{panic_msg}"),
536        (Some(v), None) | (None, Some(v)) => Some(v),
537        (None, None) => None,
538    }
539}
540
541/// Convert an Axum-style `:param` segment to a `matchit` `{param}` segment,
542/// and `*wildcard` to `{*wildcard}`.
543///
544/// Only converts leading `:` and `*` – pure literal segments are left unchanged.
545fn normalize_route_pattern(path: &str) -> String {
546    // Fast path: no special characters at all.
547    if !path.contains(':') && !path.contains('*') {
548        return path.to_string();
549    }
550    let segments: Vec<String> = path
551        .split('/')
552        .map(|segment| {
553            if segment.starts_with(':') && segment.len() > 1 {
554                format!("{{{}}}", &segment[1..])
555            } else if segment.starts_with('*') && segment.len() > 1 {
556                // matchit wildcard syntax: {*name}
557                format!("{{{segment}}}")
558            } else {
559                segment.to_string()
560            }
561        })
562        .collect();
563    segments.join("/")
564}
565
566/// Builds a `MethodRouter` that dispatches every HTTP method to the same handler —
567/// used to mount raw `tower::Service`s, which (unlike native handlers) typically do
568/// their own method matching rather than being registered per-verb.
569#[cfg(feature = "tower")]
570fn all_methods<H, S>(handler: H) -> MethodRouter<S>
571where
572    H: Handler<tower_compat::TowerServiceMarker, S>,
573    S: Clone + Send + Sync + 'static,
574{
575    any(handler)
576}
577
578impl<S> Router<S>
579where
580    S: Clone + Send + Sync + 'static,
581{
582    /// Create a new empty `Router`.
583    #[must_use]
584    pub fn new() -> Self {
585        Self {
586            routes: Vec::new(),
587            fallback: None,
588            method_not_allowed_fallback: None,
589            state: None,
590            normalize_trailing_slash: false,
591            #[cfg(feature = "tower")]
592            compiled: None,
593        }
594    }
595
596    /// Opt in to trailing-slash normalization: strips a single trailing `/`
597    /// from the incoming request's path *before* routing, so `/foo` and
598    /// `/foo/` reach the same route.
599    ///
600    /// By default (matching Axum) routing is strict — `/foo` and `/foo/` are
601    /// distinct routes, and a mismatch 404s. This opts into the common
602    /// convenience behavior natively, without pulling in `tower`/`tower-http`
603    /// just for `tower_http::normalize_path::NormalizePathLayer` — this method
604    /// mirrors that layer's own semantics exactly (an in-place path
605    /// normalization applied once per request, not a redirect), just built in.
606    ///
607    /// Only meaningful on the outermost `Router` you actually `.compile()` (or
608    /// hand to a `Server`) — like `NormalizePathLayer`, it operates on the
609    /// whole incoming request before any route matching happens, so setting it
610    /// on a router later merged/nested into another has no effect.
611    #[must_use]
612    pub const fn normalize_trailing_slash(mut self) -> Self {
613        self.normalize_trailing_slash = true;
614        self
615    }
616
617    /// Opt out of search-engine indexing — appropriate default hardening for `.onion`/`.i2p`
618    /// deployments, where a crawlable mirror is itself an unintentional discovery/deanonymization
619    /// leak (some operators don't realize search engines index onion mirrors at all).
620    ///
621    /// Adds `X-Robots-Tag: noindex, nofollow` to every response from this router (routes and
622    /// fallback alike), and — unless the app already registers its own `/robots.txt` route —
623    /// serves a blanket `User-agent: *\nDisallow: /` there too.
624    ///
625    /// # Example
626    /// ```rust
627    /// use tachyon_web::{Router, get};
628    ///
629    /// let router: Router = Router::new()
630    ///     .route("/", get(|| async { "hi" }))
631    ///     .no_index();
632    /// ```
633    #[must_use]
634    pub fn no_index(mut self) -> Self {
635        if !self.routes.iter().any(|(path, _)| path == "/robots.txt") {
636            self = self.route(
637                "/robots.txt",
638                get(|| async { "User-agent: *\nDisallow: /\n" }),
639            );
640        }
641        self.hoop(|req: Request<Body>, next: middleware::Next<S>| async move {
642            let mut resp = next.run(req).await;
643            let _ = resp.headers_mut().insert(
644                hyper::header::HeaderName::from_static("x-robots-tag"),
645                hyper::header::HeaderValue::from_static("noindex, nofollow"),
646            );
647            resp
648        })
649    }
650
651    /// Set the application state for this router, transitioning it to
652    /// another (typically `()`) state type — matching Axum's
653    /// `Router<S>::with_state<S2>(self, state: S) -> Router<S2>` signature.
654    ///
655    /// `S2` is almost always inferred as `()` since a fully-stated router is
656    /// normally handed straight to a `Server`, but leaving it generic (rather
657    /// than hardcoding `Router<()>`) matches Axum's nested-router pattern,
658    /// where an inner router's state is supplied by an outer router before
659    /// the two are merged/nested and the outer router still has its own,
660    /// different state type to resolve later.
661    #[must_use]
662    pub fn with_state<S2>(self, state: S) -> Router<S2>
663    where
664        S2: Clone + Send + Sync + 'static,
665    {
666        let state_arc = Arc::new(state);
667
668        let new_routes = self
669            .routes
670            .into_iter()
671            .map(|(path, method_router)| (path, method_router.with_state(&state_arc)))
672            .collect();
673
674        let rebind = |handler: BoxedHandler<S>| -> BoxedHandler<S2> {
675            let state_arc = state_arc.clone();
676            Arc::new(move |req, _parent_state| handler(req, state_arc.clone()))
677        };
678        let new_fallback = self.fallback.map(rebind);
679        let new_method_not_allowed_fallback = self.method_not_allowed_fallback.map(rebind);
680
681        Router {
682            routes: new_routes,
683            fallback: new_fallback,
684            method_not_allowed_fallback: new_method_not_allowed_fallback,
685            state: None,
686            normalize_trailing_slash: self.normalize_trailing_slash,
687            #[cfg(feature = "tower")]
688            compiled: None,
689        }
690    }
691
692    /// Inserts `method_router` at `path`, merging it into an already-registered
693    /// `MethodRouter` for the same path (combining non-overlapping methods
694    /// into one route) rather than always appending a new entry.
695    ///
696    /// # Panics
697    /// Panics if `method_router` defines an HTTP method already registered
698    /// for `path` — matching Axum's `Router::route`, which panics with
699    /// "Overlapping method route" in the same situation. This is a
700    /// deliberate, direct port of that panic-on-programmer-error behavior
701    /// (a route conflict is a build-time bug, not a runtime condition to
702    /// recover from), not an oversight.
703    #[allow(clippy::panic)]
704    fn push_or_merge_route(&mut self, path: String, method_router: MethodRouter<S>) {
705        #[cfg(feature = "tower")]
706        {
707            self.compiled = None;
708        }
709        if let Some(pos) = self.routes.iter().position(|(p, _)| *p == path) {
710            let (_, existing) = self.routes.remove(pos);
711            let merged = existing
712                .merge(method_router, &path)
713                .unwrap_or_else(|e| panic!("{e}"));
714            self.routes.insert(pos, (path, merged));
715        } else {
716            self.routes.push((path, method_router));
717        }
718    }
719
720    /// Add a route to the router.
721    ///
722    /// Registering the same path more than once merges the method routers —
723    /// e.g. `.route("/x", get(a)).route("/x", post(b))` yields one route that
724    /// answers both `GET` and `POST` — matching Axum. Registering the *same*
725    /// method for the same path twice panics, also matching Axum.
726    #[must_use]
727    pub fn route(mut self, path: &str, method_router: MethodRouter<S>) -> Self {
728        let normalized = normalize_route_pattern(path);
729        self.push_or_merge_route(normalized, method_router);
730        self
731    }
732
733    /// A dummy/compatibility method that returns the router itself, matching Axum's API
734    /// when preparing a router to be run with a server listener.
735    #[must_use]
736    pub const fn into_make_service(self) -> Self {
737        self
738    }
739
740    /// Serve an entire directory as static files — the simplest, Nginx-like API.
741    ///
742    /// Serves directly from disk on every request; it does not call
743    /// [`static_dir::ServeDir::preload`], so `CacheConfig::enabled`'s default of
744    /// `true` has no effect here. Use [`serve_dir`](Self::serve_dir) with a
745    /// manually-preloaded `ServeDir` if you want the in-memory RAM cache.
746    ///
747    /// Do not point `dir_path` at a directory that can ever contain files an
748    /// untrusted user chose the bytes of (e.g. an upload folder mixed into the
749    /// served tree) — see [`static_dir::ServeDir`]'s docs for why (in short: a
750    /// user-supplied `.svg` served this way can carry an executable
751    /// `<script>`).
752    ///
753    /// # Example
754    /// ```rust,no_run
755    /// use tachyon_web::Router;
756    ///
757    /// // Serve ./public/ at /, with index.html as the default.
758    /// let router = Router::new()
759    ///     .serve_static("./public");
760    /// # let _ = router.with_state::<()>(());
761    /// ```
762    #[must_use]
763    pub fn serve_static(self, dir_path: impl AsRef<std::path::Path>) -> Self {
764        let sd = static_dir::ServeDir::new(&dir_path).index("index.html");
765        self.serve_dir("/", sd)
766    }
767
768    /// Serve an entire static directory under a URL prefix with full configuration control.
769    ///
770    /// Registers both an exact route (`prefix`) and a wildcard route (`prefix/*path`).
771    /// Use `serve_static()` for the common case of serving a dir at `/`. See
772    /// [`static_dir::ServeDir`]'s docs for the upload-safety warning before
773    /// serving a directory that can contain user-supplied files.
774    #[must_use]
775    pub fn serve_dir(mut self, prefix: &str, serve_dir: static_dir::ServeDir) -> Self {
776        let prefix = prefix.trim_end_matches('/');
777        let exact_route = if prefix.is_empty() { "/" } else { prefix };
778        let wildcard_route = format!("{prefix}/*path");
779
780        self = self.route(exact_route, serve_dir.clone().into_method_router());
781        self = self.route(&wildcard_route, serve_dir.into_method_router());
782        self
783    }
784
785    /// Natively serve a specific file on a specific route.
786    ///
787    /// The file is read **once at startup** into a `Bytes` buffer. Every subsequent
788    /// request is served from that buffer with **zero I/O and zero allocations**,
789    /// rivalling `include_bytes!` without inflating the binary.
790    ///
791    /// # Errors
792    /// Returns an `Err` if the file cannot be read at startup.
793    pub fn serve_file(self, path: &str, file_path: &str) -> Result<Self, std::io::Error> {
794        let content = std::fs::read(file_path)?;
795        let content_bytes = Bytes::from(content);
796        let mime_type = static_dir::guess_mime_type(std::path::Path::new(file_path));
797
798        Ok(self.route(
799            path,
800            get(move |_req: Request<Body>| {
801                let body_content = content_bytes.clone();
802                async move {
803                    let mut resp = Response::new(Body::full(body_content));
804                    let mime_val = hyper::header::HeaderValue::from_static(mime_type);
805                    let _ = resp
806                        .headers_mut()
807                        .insert(hyper::header::CONTENT_TYPE, mime_val);
808                    resp
809                }
810            }),
811        ))
812    }
813
814    /// Natively serve a specific file on a specific route dynamically.
815    ///
816    /// The file is read from disk on every request. Ideal for large files that
817    /// change frequently where startup preloading is undesirable.
818    #[must_use]
819    pub fn serve_file_dynamic(self, path: &str, file_path: &str) -> Self {
820        let file_path_str = file_path.to_string();
821        let mime_type = static_dir::guess_mime_type(std::path::Path::new(file_path));
822
823        self.route(
824            path,
825            get(move |_req: Request<Body>| {
826                let fp = file_path_str.clone();
827                async move {
828                    let Ok(content) = tokio::fs::read(&fp).await else {
829                        let mut resp = Response::new(Body::empty());
830                        *resp.status_mut() = StatusCode::NOT_FOUND;
831                        return resp;
832                    };
833                    let mut resp = Response::new(Body::full(Bytes::from(content)));
834                    let mime_val = hyper::header::HeaderValue::from_static(mime_type);
835                    let _ = resp
836                        .headers_mut()
837                        .insert(hyper::header::CONTENT_TYPE, mime_val);
838                    resp
839                }
840            }),
841        )
842    }
843
844    /// Nest another router under a given path prefix.
845    ///
846    /// Seamlessly merges all routes from the sub-router into this router.
847    ///
848    /// Matches Axum: handlers inside the nested router see a request `Uri` with
849    /// `prefix` stripped (e.g. a request to `/api/users/1` nested under `/api`
850    /// sees `/users/1`), while [`crate::routing::extract::OriginalUri`] recovers
851    /// the pre-strip, full path. Nesting is resolved once at `compile()` time —
852    /// there's no per-request recursive dispatch — so this is exactly as fast as
853    /// a flat route table; only the one matched route's prefix is ever stripped.
854    ///
855    /// # Deviation from Axum: the inner router's own `fallback` is not carried over
856    ///
857    /// In real Axum, nesting mounts the whole inner `Router` as a recursive sub-service, so a
858    /// request under `prefix` that the inner router's own routes don't match still reaches the
859    /// *inner* router's `fallback` before ever falling through to the outer one. Because this
860    /// implementation flattens the inner router's routes into the same top-level route table
861    /// (the design that keeps nesting as fast as a flat lookup — see above), there is no
862    /// separate inner dispatch step left for a per-nest fallback to hook into: any path under
863    /// `prefix` that isn't one of the inner router's own registered routes simply falls through
864    /// to whatever `Router::fallback` (or the default `404`) is configured on the *outermost*
865    /// router — the inner router's own `.fallback(...)`, if it set one, is never called. Use
866    /// [`Router::fallback`] on the outer router (or register an explicit catch-all route under
867    /// `prefix`) if you need a per-module 404 handler.
868    #[must_use]
869    pub fn nest(mut self, prefix: &str, mut router: Self) -> Self {
870        let prefix = prefix.trim_end_matches('/');
871        for (path, mut method_router) in router.routes.drain(..) {
872            let nested_path = if path == "/" || path.is_empty() {
873                prefix.to_string()
874            } else {
875                format!("{prefix}{path}")
876            };
877            let final_path = if nested_path.is_empty() {
878                "/".to_string()
879            } else {
880                nested_path
881            };
882            // Accumulate the strip prefix across multiple levels of nesting
883            // (e.g. `.nest("/api", Router::new().nest("/v1", inner))` strips
884            // `/api/v1`, not just `/v1`).
885            let accumulated = method_router.nest_prefix.as_ref().map_or_else(
886                || prefix.to_string(),
887                |existing| format!("{prefix}{existing}"),
888            );
889            method_router.nest_prefix = Some(Arc::from(accumulated));
890            self.push_or_merge_route(final_path, method_router);
891        }
892        self
893    }
894
895    /// Merge another router's routes into this router.
896    ///
897    /// If exactly one of the two routers has a [`fallback`](Self::fallback) (or a
898    /// [`method_not_allowed_fallback`](Self::method_not_allowed_fallback)) configured, the
899    /// merged router adopts it — matching Axum, which does the same for `Router::fallback`.
900    /// Unlike [`nest`](Self::nest) (where the inner router's fallback is deliberately never
901    /// reachable — it would only ever fire for a request the outer router's own routing
902    /// already decided was unmatched, which the outer fallback already handles), `merge`
903    /// treats both routers as peers, so silently dropping one side's fallback would silently
904    /// change which handler answers unmatched requests.
905    ///
906    /// # Panics
907    /// Panics if `other` defines a method for a path already registered in `self`, or if
908    /// both routers already have a `fallback`/`method_not_allowed_fallback` configured —
909    /// matching Axum's `Router::merge`.
910    #[must_use]
911    pub fn merge(mut self, mut other: Self) -> Self {
912        #[cfg(feature = "tower")]
913        {
914            self.compiled = None;
915        }
916        for (path, method_router) in other.routes.drain(..) {
917            self.push_or_merge_route(path, method_router);
918        }
919        self.fallback = merge_optional(
920            self.fallback.take(),
921            other.fallback.take(),
922            "Cannot merge two `Router`s that both have a fallback",
923        );
924        self.method_not_allowed_fallback = merge_optional(
925            self.method_not_allowed_fallback.take(),
926            other.method_not_allowed_fallback.take(),
927            "Cannot merge two `Router`s that both have a method_not_allowed_fallback",
928        );
929        self
930    }
931
932    /// Mount a raw `tower::Service` at `path`, handling every HTTP method.
933    ///
934    /// Requires the `tower` feature. Prefer `.route(path, get(handler))` with a native
935    /// handler where possible — this exists to bridge in pre-built Tower/tower-http
936    /// services (e.g. `tower_http::services::ServeFile`) without a rewrite.
937    #[cfg(feature = "tower")]
938    #[must_use]
939    pub fn route_service<Svc, RespBody>(self, path: &str, service: Svc) -> Self
940    where
941        Svc: tower::Service<Request<Bytes>, Response = Response<RespBody>>
942            + Clone
943            + Send
944            + Sync
945            + 'static,
946        Svc::Future: Send + 'static,
947        Svc::Error: Into<crate::http::error::Error> + Send,
948        RespBody: hyper::body::Body<Data = Bytes> + Send + 'static,
949        RespBody::Error: Into<crate::http::error::Error>,
950    {
951        let handler = tower_compat::ServiceHandler {
952            service,
953            strip_prefix: None,
954        };
955        self.route(path, all_methods(handler))
956    }
957
958    /// Nest a raw `tower::Service` under `prefix`, with the mounted path rewritten
959    /// relative to `prefix` before the service sees it (matching Axum's `nest_service`).
960    ///
961    /// Requires the `tower` feature.
962    #[must_use]
963    #[cfg(feature = "tower")]
964    pub fn nest_service<Svc, RespBody>(self, prefix: &str, service: Svc) -> Self
965    where
966        Svc: tower::Service<Request<Bytes>, Response = Response<RespBody>>
967            + Clone
968            + Send
969            + Sync
970            + 'static,
971        Svc::Future: Send + 'static,
972        Svc::Error: Into<crate::http::error::Error> + Send,
973        RespBody: hyper::body::Body<Data = Bytes> + Send + 'static,
974        RespBody::Error: Into<crate::http::error::Error>,
975    {
976        let prefix = prefix.trim_end_matches('/');
977        let exact = if prefix.is_empty() { "/" } else { prefix };
978        let wildcard = format!("{prefix}/*__tachyon_nest_rest");
979        let handler = tower_compat::ServiceHandler {
980            service,
981            strip_prefix: Some(Arc::from(prefix)),
982        };
983        self.route(exact, all_methods(handler.clone()))
984            .route(&wildcard, all_methods(handler))
985    }
986
987    /// Set a raw `tower::Service` as the fallback for unmatched paths.
988    ///
989    /// Requires the `tower` feature.
990    #[must_use]
991    #[cfg(feature = "tower")]
992    pub fn fallback_service<Svc, RespBody>(mut self, service: Svc) -> Self
993    where
994        Svc: tower::Service<Request<Bytes>, Response = Response<RespBody>>
995            + Clone
996            + Send
997            + Sync
998            + 'static,
999        Svc::Future: Send + 'static,
1000        Svc::Error: Into<crate::http::error::Error> + Send,
1001        RespBody: hyper::body::Body<Data = Bytes> + Send + 'static,
1002        RespBody::Error: Into<crate::http::error::Error>,
1003    {
1004        let handler = tower_compat::ServiceHandler {
1005            service,
1006            strip_prefix: None,
1007        };
1008        self.fallback = Some(Arc::new(move |req, state| handler.clone().call(req, state)));
1009        self.compiled = None;
1010        self
1011    }
1012
1013    /// Apply a `tower::Layer` to every route **and** the fallback in this router.
1014    ///
1015    /// Requires the `tower` feature. Prefer `.hoop()`/`.hoop_at()` for new code — this
1016    /// exists to bridge in existing Tower/tower-http layers (tracing, compression,
1017    /// timeouts) without rewriting them as native middleware.
1018    #[must_use]
1019    #[cfg(feature = "tower")]
1020    pub fn layer<L, RespBody>(self, layer: L) -> Self
1021    where
1022        L: tower::Layer<tower_compat::NextService<S>> + Clone + Send + Sync + 'static,
1023        L::Service: tower::Service<Request<Bytes>, Response = Response<RespBody>> + Send + 'static,
1024        <L::Service as tower::Service<Request<Bytes>>>::Future: Send + 'static,
1025        <L::Service as tower::Service<Request<Bytes>>>::Error:
1026            Into<crate::http::error::Error> + Send,
1027        RespBody: hyper::body::Body<Data = Bytes> + Send + 'static,
1028        RespBody::Error: Into<crate::http::error::Error>,
1029    {
1030        self.hoop_at(
1031            middleware::MiddlewarePosition::First,
1032            tower_compat::from_tower_layer(layer),
1033        )
1034    }
1035
1036    /// Apply a `tower::Layer` to every registered route, but *not* the fallback —
1037    /// matching Axum's distinction between `.layer()` and `.route_layer()`.
1038    ///
1039    /// Requires the `tower` feature.
1040    #[must_use]
1041    #[cfg(feature = "tower")]
1042    pub fn route_layer<L, RespBody>(mut self, layer: L) -> Self
1043    where
1044        L: tower::Layer<tower_compat::NextService<S>> + Clone + Send + Sync + 'static,
1045        L::Service: tower::Service<Request<Bytes>, Response = Response<RespBody>> + Send + 'static,
1046        <L::Service as tower::Service<Request<Bytes>>>::Future: Send + 'static,
1047        <L::Service as tower::Service<Request<Bytes>>>::Error:
1048            Into<crate::http::error::Error> + Send,
1049        RespBody: hyper::body::Body<Data = Bytes> + Send + 'static,
1050        RespBody::Error: Into<crate::http::error::Error>,
1051    {
1052        let mw = tower_compat::from_tower_layer(layer);
1053        for (_path, method_router) in &mut self.routes {
1054            let m = mw.clone();
1055            let old_mr = std::mem::take(method_router);
1056            *method_router = old_mr.hoop_at(middleware::MiddlewarePosition::Last, m);
1057        }
1058        self.compiled = None;
1059        self
1060    }
1061
1062    /// Set a custom fallback handler for requests that don't match any route.
1063    #[must_use]
1064    pub fn fallback<H, T>(mut self, handler: H) -> Self
1065    where
1066        H: Handler<T, S>,
1067        T: 'static,
1068    {
1069        self.fallback = Some(Arc::new(move |req, state| {
1070            let handler = handler.clone();
1071            handler.call(req, state)
1072        }));
1073        #[cfg(feature = "tower")]
1074        {
1075            self.compiled = None;
1076        }
1077        self
1078    }
1079
1080    /// Set a custom fallback handler for requests whose path matches a route but
1081    /// whose method has no registered handler (the default is a bare `405 Method
1082    /// Not Allowed` with an `Allow` header).
1083    #[must_use]
1084    pub fn method_not_allowed_fallback<H, T>(mut self, handler: H) -> Self
1085    where
1086        H: Handler<T, S>,
1087        T: 'static,
1088    {
1089        self.method_not_allowed_fallback = Some(Arc::new(move |req, state| {
1090            let handler = handler.clone();
1091            handler.call(req, state)
1092        }));
1093        #[cfg(feature = "tower")]
1094        {
1095            self.compiled = None;
1096        }
1097        self
1098    }
1099
1100    /// Apply a middleware handler to ALL routes and the fallback registered in this `Router`.
1101    #[must_use]
1102    pub fn hoop<F, Fut, Res>(self, middleware: F) -> Self
1103    where
1104        F: Fn(Request<Body>, middleware::Next<S>) -> Fut + Clone + Send + Sync + 'static,
1105        Fut: Future<Output = Res> + Send + 'static,
1106        Res: IntoResponse + Send + 'static,
1107    {
1108        self.hoop_at(middleware::MiddlewarePosition::First, middleware)
1109    }
1110
1111    /// Apply a middleware handler at a specific position (First/Last) to ALL routes and the fallback.
1112    #[must_use]
1113    pub fn hoop_at<F, Fut, Res>(
1114        mut self,
1115        position: middleware::MiddlewarePosition,
1116        middleware: F,
1117    ) -> Self
1118    where
1119        F: Fn(Request<Body>, middleware::Next<S>) -> Fut + Clone + Send + Sync + 'static,
1120        Fut: Future<Output = Res> + Send + 'static,
1121        Res: IntoResponse + Send + 'static,
1122    {
1123        #[cfg(feature = "tower")]
1124        {
1125            self.compiled = None;
1126        }
1127        for (_path, method_router) in &mut self.routes {
1128            let mw = middleware.clone();
1129            let old_mr = std::mem::take(method_router);
1130            *method_router = old_mr.hoop_at(position, mw);
1131        }
1132
1133        if let Some(fallback) = self.fallback.take() {
1134            let mw = middleware.clone();
1135            self.fallback = Some(Arc::new(move |req, state| {
1136                let next = middleware::Next {
1137                    handler: fallback.clone(),
1138                    state,
1139                };
1140                let fut = mw(req, next);
1141                crate::routing::handler::ResponseFuture::Boxed(Box::pin(async move {
1142                    fut.await.into_response()
1143                }))
1144            }));
1145        }
1146
1147        if let Some(handler) = self.method_not_allowed_fallback.take() {
1148            let mw = middleware;
1149            self.method_not_allowed_fallback = Some(Arc::new(move |req, state| {
1150                let next = middleware::Next {
1151                    handler: handler.clone(),
1152                    state,
1153                };
1154                let fut = mw(req, next);
1155                crate::routing::handler::ResponseFuture::Boxed(Box::pin(async move {
1156                    fut.await.into_response()
1157                }))
1158            }));
1159        }
1160
1161        self
1162    }
1163
1164    /// Route an incoming request directly, compiling the router on the fly.
1165    /// Primarily useful for testing.
1166    ///
1167    /// # Panics
1168    /// Panics if router compilation fails (e.g. a duplicate route was registered).
1169    #[allow(clippy::expect_used)]
1170    pub async fn handle_request(&self, req: Request<Body>) -> Response<Body>
1171    where
1172        S: Default,
1173    {
1174        let compiled = self.clone().compile().expect("Router compilation failed");
1175        compiled.handle_request(req).await
1176    }
1177
1178    /// Build and compile the routing tree, returning a `CompiledRouter`.
1179    ///
1180    /// # Errors
1181    /// Returns `RouterError::DuplicateRoute` if the same literal path somehow
1182    /// reaches `compile()` twice. In practice this can't happen through the
1183    /// public API — `route()`/`nest()`/`merge()` all merge same-path entries
1184    /// (panicking on overlapping methods, matching Axum) — this is an
1185    /// internal invariant check, not a condition callers need to handle.
1186    pub fn compile(self) -> Result<CompiledRouter<S>, RouterError>
1187    where
1188        S: Default,
1189    {
1190        let mut matcher: matchit::Router<MethodRouter<S>> = matchit::Router::new();
1191
1192        let mut seen = std::collections::HashSet::new();
1193        for (path, mut method_router) in self.routes {
1194            if !seen.insert(path.clone()) {
1195                return Err(RouterError::DuplicateRoute(path));
1196            }
1197            method_router.compile_in_place();
1198            method_router.param_names = extract_param_names(&path);
1199            method_router.matched_path = Arc::from(path.as_str());
1200            matcher.insert(path, method_router)?;
1201        }
1202
1203        let state = self.state.unwrap_or_else(|| Arc::new(S::default()));
1204
1205        Ok(CompiledRouter {
1206            matcher,
1207            fallback: self.fallback,
1208            method_not_allowed_fallback: self.method_not_allowed_fallback,
1209            state,
1210            normalize_trailing_slash: self.normalize_trailing_slash,
1211        })
1212    }
1213}
1214
1215/// Errors that can occur during router construction or compilation.
1216#[derive(Debug)]
1217pub enum RouterError {
1218    /// Duplicate route registered.
1219    DuplicateRoute(String),
1220    /// The same path was registered with the same HTTP method more than
1221    /// once (via separate `.route()` calls) — matching Axum's "Overlapping
1222    /// method route" panic. Registering the same path with *different*
1223    /// methods across multiple `.route()` calls is fine and merges into one
1224    /// route, exactly like Axum.
1225    MethodOverlap {
1226        /// The HTTP method that was registered twice.
1227        method: &'static str,
1228        /// The path it was registered twice for.
1229        path: String,
1230    },
1231    /// matchit insert error.
1232    Insert(matchit::InsertError),
1233}
1234
1235impl std::fmt::Display for RouterError {
1236    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1237        match self {
1238            Self::DuplicateRoute(path) => write!(f, "Duplicate route path registered: '{path}'"),
1239            Self::MethodOverlap { method, path } => {
1240                write!(
1241                    f,
1242                    "Overlapping method route: {method} {path} already exists"
1243                )
1244            }
1245            Self::Insert(e) => write!(f, "Router insert error: {e}"),
1246        }
1247    }
1248}
1249
1250impl std::error::Error for RouterError {}
1251
1252impl From<matchit::InsertError> for RouterError {
1253    fn from(e: matchit::InsertError) -> Self {
1254        Self::Insert(e)
1255    }
1256}
1257
1258// ─── CompiledRouter ───────────────────────────────────────────────────────────
1259
1260/// A compiled routing table ready to serve requests.
1261#[derive(Clone)]
1262pub struct CompiledRouter<S> {
1263    matcher: matchit::Router<MethodRouter<S>>,
1264    fallback: Option<BoxedHandler<S>>,
1265    method_not_allowed_fallback: Option<BoxedHandler<S>>,
1266    state: Arc<S>,
1267    normalize_trailing_slash: bool,
1268}
1269
1270impl<S> std::fmt::Debug for CompiledRouter<S> {
1271    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1272        f.debug_struct("CompiledRouter")
1273            .field("has_fallback", &self.fallback.is_some())
1274            .field(
1275                "has_method_not_allowed_fallback",
1276                &self.method_not_allowed_fallback.is_some(),
1277            )
1278            .finish_non_exhaustive()
1279    }
1280}
1281
1282/// Parses the `{name}` / `{*name}` placeholders out of a compiled route
1283/// pattern, in declaration order, matching the order `matchit` yields them
1284/// in `Match::params`. Computed once per route at `compile()` time so the
1285/// hot path (`resolve()`) never has to allocate a `String` for a param key.
1286fn extract_param_names(path: &str) -> Arc<[Arc<str>]> {
1287    let mut names = Vec::new();
1288    let bytes = path.as_bytes();
1289    let mut i = 0;
1290    while i < bytes.len() {
1291        if bytes[i] == b'{'
1292            && let Some(end) = path[i + 1..].find('}')
1293        {
1294            let inner = &path[i + 1..i + 1 + end];
1295            let name = inner.strip_prefix('*').unwrap_or(inner);
1296            names.push(Arc::from(name));
1297            i += 1 + end + 1;
1298        } else {
1299            i += 1;
1300        }
1301    }
1302    Arc::from(names)
1303}
1304
1305/// Zero-allocation percent-decoding helper for path parameters.
1306///
1307/// Returns `None` if the input contains invalid percent encoding.
1308pub(crate) fn percent_decode(s: &str) -> Option<std::borrow::Cow<'_, str>> {
1309    let bytes = s.as_bytes();
1310    if !bytes.contains(&b'%') {
1311        return Some(std::borrow::Cow::Borrowed(s));
1312    }
1313    let mut decoded = Vec::with_capacity(bytes.len());
1314    let mut i = 0;
1315    while i < bytes.len() {
1316        if bytes[i] == b'%' {
1317            if i + 2 < bytes.len() {
1318                let hex = &bytes[i + 1..i + 3];
1319                if let Ok(hex_str) = std::str::from_utf8(hex)
1320                    && let Ok(val) = u8::from_str_radix(hex_str, 16)
1321                {
1322                    decoded.push(val);
1323                    i += 3;
1324                    continue;
1325                }
1326            }
1327            return None; // invalid percent encoding
1328        }
1329        decoded.push(bytes[i]);
1330        i += 1;
1331    }
1332    let s = String::from_utf8(decoded).ok()?;
1333    Some(std::borrow::Cow::Owned(s))
1334}
1335
1336/// Strips `prefix` from `req`'s `Uri` path in place, used by nested routers
1337/// (native [`Router::nest`] and [`tower_compat::ServiceHandler`]'s
1338/// `nest_service`) to match Axum's nested-router URI rewriting.
1339///
1340/// The prefix is stripped directly from the request's raw (still
1341/// percent-encoded) `path()` string, and the original query string is carried
1342/// over untouched — this deliberately avoids reconstructing the URI from a
1343/// percent-*decoded* path segment, which would let a client smuggle a `?`/`#`
1344/// past routing by percent-encoding it (e.g. `/api/foo%3Fadmin=1` decoding into
1345/// a synthesized `?admin=1` query the router never evaluated as one).
1346pub(crate) fn strip_uri_prefix(req: &mut Request<Body>, prefix: &str) {
1347    let path = req.uri().path();
1348    let stripped = path.strip_prefix(prefix).unwrap_or(path);
1349    let new_path = if stripped.starts_with('/') {
1350        stripped.to_string()
1351    } else {
1352        format!("/{stripped}")
1353    };
1354    let path_and_query = match req.uri().query() {
1355        Some(q) if !q.is_empty() => format!("{new_path}?{q}"),
1356        _ => new_path,
1357    };
1358    let mut parts = req.uri().clone().into_parts();
1359    if let Ok(pq) = path_and_query.parse() {
1360        parts.path_and_query = Some(pq);
1361    }
1362    if let Ok(new_uri) = hyper::Uri::from_parts(parts) {
1363        *req.uri_mut() = new_uri;
1364    }
1365}
1366
1367/// Strips a single trailing `/` from `req`'s `Uri` path in place (used by
1368/// [`Router::normalize_trailing_slash`]), preserving the query string and
1369/// never stripping the root `/` itself. Mirrors
1370/// `tower_http::normalize_path::NormalizePathLayer`'s own behavior: an
1371/// in-place normalization applied before routing, not a redirect.
1372fn strip_trailing_slash(req: &mut Request<Body>) {
1373    let path = req.uri().path();
1374    if path.len() <= 1 || !path.ends_with('/') {
1375        return;
1376    }
1377    let new_path = &path[..path.len() - 1];
1378    let path_and_query = match req.uri().query() {
1379        Some(q) if !q.is_empty() => format!("{new_path}?{q}"),
1380        _ => new_path.to_string(),
1381    };
1382    let mut parts = req.uri().clone().into_parts();
1383    if let Ok(pq) = path_and_query.parse() {
1384        parts.path_and_query = Some(pq);
1385    }
1386    if let Ok(new_uri) = hyper::Uri::from_parts(parts) {
1387        *req.uri_mut() = new_uri;
1388    }
1389}
1390
1391impl<S> CompiledRouter<S>
1392where
1393    S: Clone + Send + Sync + 'static,
1394{
1395    /// Route an incoming request, returning the resulting HTTP response.
1396    ///
1397    /// # Trailing-slash handling
1398    ///
1399    /// Tachyon follows the same semantics as Axum: routes are matched **exactly**.
1400    /// `/foo` and `/foo/` are distinct routes — a request for one does **not**
1401    /// fall back to a route registered under the other, unless the router opted
1402    /// into [`Router::normalize_trailing_slash`], in which case a trailing `/`
1403    /// is stripped from the request path before matching (in-place, not a
1404    /// redirect — the same behavior as `tower_http`'s `NormalizePathLayer`). We
1405    /// also do **not** perform case-folding, which would silently obscure bugs
1406    /// and change observable behavior.
1407    ///
1408    /// This is the hot path: the `matchit` radix-tree lookup is `O(path_len)`,
1409    /// challenge interception is a single prefix check, and method dispatch
1410    /// is an array index — all zero-allocation on the happy path.
1411    #[inline]
1412    pub async fn handle_request(&self, req: Request<Body>) -> Response<Body> {
1413        let mut req = req;
1414
1415        if self.normalize_trailing_slash {
1416            strip_trailing_slash(&mut req);
1417        }
1418
1419        let path = req.uri().path();
1420
1421        #[cfg(feature = "lets-encrypt")]
1422        if path.starts_with("/.well-known/acme-challenge/") {
1423            use hyper::StatusCode;
1424            let token = path
1425                .strip_prefix("/.well-known/acme-challenge/")
1426                .unwrap_or("");
1427            if let Some(key_auth) = crate::tls::acme::get_challenge(token) {
1428                return Response::builder()
1429                    .status(StatusCode::OK)
1430                    .header(hyper::header::CONTENT_TYPE, "text/plain")
1431                    .body(Body::full(Bytes::copy_from_slice(key_auth.as_bytes())))
1432                    .unwrap_or_else(|_| Response::new(Body::empty()));
1433            }
1434        }
1435
1436        let (method_router, params): RouteResolution<'_, S> = match self.resolve(path) {
1437            Some(r) => r,
1438            None => {
1439                return if let Some(fb) = &self.fallback {
1440                    fb(req, self.state.clone()).await
1441                } else {
1442                    Response::builder()
1443                        .status(StatusCode::NOT_FOUND)
1444                        .body(Body::full(Bytes::from_static(b"Not Found")))
1445                        .unwrap_or_else(|_| Response::new(Body::empty()))
1446                };
1447            }
1448        };
1449
1450        // Insert path params into extensions before handing off to the handler.
1451        // Skip the extension insert entirely on parameterless routes (common case).
1452        if !params.is_empty() {
1453            let _ = req.extensions_mut().insert(PathParams(params));
1454        }
1455
1456        #[cfg(feature = "matched-path")]
1457        {
1458            let _ = req
1459                .extensions_mut()
1460                .insert(crate::routing::extract::MatchedPath(
1461                    method_router.matched_path.clone(),
1462                ));
1463        }
1464
1465        // If this route was reached through `Router::nest()`, strip the
1466        // accumulated prefix from the `Uri` the handler sees, preserving the
1467        // pre-strip path via `OriginalUri` — matching Axum's nested-router
1468        // semantics (see `Router::nest`'s docs).
1469        if let Some(prefix) = &method_router.nest_prefix {
1470            #[cfg(feature = "original-uri")]
1471            {
1472                let original_uri = req.uri().clone();
1473                let _ = req
1474                    .extensions_mut()
1475                    .insert(crate::routing::extract::OriginalUri(original_uri));
1476            }
1477            strip_uri_prefix(&mut req, prefix);
1478        }
1479
1480        let method = req.method();
1481        let idx = method_index(method);
1482
1483        // HEAD uses an explicit HEAD handler when registered; otherwise it
1484        // falls back to the GET handler, with the body discarded afterwards.
1485        let is_head = *method == Method::HEAD;
1486        let falls_back_to_get =
1487            is_head && method_router.handlers[IDX_HEAD].is_none() && idx == Some(IDX_HEAD);
1488        let effective_idx = if falls_back_to_get {
1489            Some(IDX_GET)
1490        } else {
1491            idx
1492        };
1493
1494        let handler = effective_idx.and_then(|i| method_router.handlers[i].as_ref());
1495
1496        if let Some(h) = handler {
1497            let mut resp = h.call(req, self.state.clone()).await;
1498            // Per HTTP semantics, a HEAD response must never carry a body,
1499            // regardless of whether it came from an explicit HEAD handler or
1500            // the implicit GET fallback.
1501            if is_head {
1502                *resp.body_mut() = Body::empty();
1503            }
1504            resp
1505        } else if let Some(fb) = &self.method_not_allowed_fallback {
1506            // Route exists but this method has no handler, and a custom fallback
1507            // was configured for that case via `Router::method_not_allowed_fallback`.
1508            fb(req, self.state.clone()).await
1509        } else {
1510            // Route exists but this method has no handler → 405 with Allow header.
1511            let allow = method_router.allow_header();
1512            Response::builder()
1513                .status(StatusCode::METHOD_NOT_ALLOWED)
1514                .header(hyper::header::ALLOW, &allow)
1515                .body(Body::full(Bytes::from_static(b"Method Not Allowed")))
1516                .unwrap_or_else(|_| Response::new(Body::empty()))
1517        }
1518    }
1519
1520    /// Internal: attempt to match `path`, returning `RouteResolution`.
1521    #[inline]
1522    fn resolve(&self, path: &str) -> Option<RouteResolution<'_, S>> {
1523        let m = self.matcher.at(path).ok()?;
1524        let params = if m.params.is_empty() {
1525            Vec::new()
1526        } else {
1527            // Param names are precomputed once at `compile()` time and cloning an
1528            // `Arc<str>` is a refcount bump, not a heap allocation — only the
1529            // decoded value needs to be freshly allocated per request.
1530            let names = &m.value.param_names;
1531            let mut p = Vec::with_capacity(m.params.len());
1532            for (name, (_, v)) in names.iter().zip(m.params.iter()) {
1533                let decoded =
1534                    percent_decode(v).map_or_else(|| v.to_string(), std::borrow::Cow::into_owned);
1535                p.push((name.clone(), decoded));
1536            }
1537            p
1538        };
1539        Some((m.value, params))
1540    }
1541}
1542
1543/// Type alias for matched route results to keep signatures clean.
1544pub type RouteResolution<'a, S> = (&'a MethodRouter<S>, Vec<(Arc<str>, String)>);
1545
1546// ─── Unit tests ───────────────────────────────────────────────────────────────
1547
1548#[cfg(test)]
1549mod tests {
1550    #![allow(clippy::unwrap_used)]
1551    use super::*;
1552    use crate::routing::extract::Path;
1553    use serde::Deserialize;
1554
1555    #[derive(Debug, Deserialize)]
1556    struct IdParam {
1557        id: u32,
1558    }
1559
1560    async fn handle_root() -> &'static str {
1561        "root"
1562    }
1563    async fn handle_id(Path(p): Path<IdParam>) -> String {
1564        format!("id:{}", p.id)
1565    }
1566    async fn handle_post() -> &'static str {
1567        "post"
1568    }
1569    async fn handle_delete() -> &'static str {
1570        "deleted"
1571    }
1572
1573    fn make_req(method: &str, uri: &str) -> Request<Body> {
1574        Request::builder()
1575            .method(method)
1576            .uri(uri)
1577            .body(Body::empty())
1578            .expect("valid request")
1579    }
1580
1581    fn compile_app() -> CompiledRouter<()> {
1582        Router::new()
1583            .route("/", get(handle_root))
1584            .route(
1585                "/user/:id",
1586                get(handle_id).post(handle_post).delete(handle_delete),
1587            )
1588            .with_state::<()>(())
1589            .compile()
1590            .expect("compile router")
1591    }
1592
1593    // ── routing correctness ──────────────────────────────────────────────────
1594
1595    #[tokio::test]
1596    async fn test_root_route() {
1597        let router = compile_app();
1598        let resp = router.handle_request(make_req("GET", "/")).await;
1599        assert_eq!(resp.status(), StatusCode::OK);
1600    }
1601
1602    #[tokio::test]
1603    async fn test_path_param_extraction() {
1604        use http_body_util::BodyExt;
1605        let router = compile_app();
1606        let resp = router.handle_request(make_req("GET", "/user/42")).await;
1607        assert_eq!(resp.status(), StatusCode::OK);
1608        let body = resp.into_body().collect().await.unwrap().to_bytes();
1609        assert_eq!(body.as_ref(), b"id:42");
1610    }
1611
1612    #[tokio::test]
1613    async fn test_not_found() {
1614        let router = compile_app();
1615        let resp = router.handle_request(make_req("GET", "/nonexistent")).await;
1616        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1617    }
1618
1619    #[tokio::test]
1620    async fn test_method_not_allowed_has_allow_header() {
1621        let router = compile_app();
1622        // /user/:id has GET, POST, DELETE — PATCH is not registered
1623        let resp = router.handle_request(make_req("PATCH", "/user/1")).await;
1624        assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
1625        let allow = resp
1626            .headers()
1627            .get(hyper::header::ALLOW)
1628            .expect("Allow header must be present");
1629        let allow_str = allow.to_str().expect("valid utf8");
1630        assert!(allow_str.contains("GET"), "Allow: {allow_str}");
1631        assert!(allow_str.contains("POST"), "Allow: {allow_str}");
1632        assert!(allow_str.contains("DELETE"), "Allow: {allow_str}");
1633        assert!(!allow_str.contains("PATCH"), "Allow: {allow_str}");
1634    }
1635
1636    // ── trailing slash strictness (matches Axum: /foo and /foo/ are distinct) ──
1637
1638    #[tokio::test]
1639    async fn test_trailing_slash_not_stripped() {
1640        let router = compile_app();
1641        // "/user/5/" must NOT match "/user/:id" — Axum treats these as distinct routes.
1642        let resp = router.handle_request(make_req("GET", "/user/5/")).await;
1643        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1644    }
1645
1646    #[tokio::test]
1647    async fn test_trailing_slash_not_added() {
1648        // register route with trailing slash, request without — must NOT match.
1649        let router = Router::new()
1650            .route("/about/", get(handle_root))
1651            .with_state::<()>(())
1652            .compile()
1653            .expect("compile");
1654        let resp = router.handle_request(make_req("GET", "/about")).await;
1655        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1656    }
1657
1658    // ── case sensitivity ──────────────────────────────────────────────────────
1659
1660    #[tokio::test]
1661    async fn test_case_sensitive_routing() {
1662        // Paths must match exactly — no silent case-folding.
1663        let router = compile_app();
1664        let resp = router.handle_request(make_req("GET", "/User/1")).await;
1665        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1666    }
1667
1668    // ── fallback ──────────────────────────────────────────────────────────────
1669
1670    #[tokio::test]
1671    async fn test_custom_fallback() {
1672        let router = Router::new()
1673            .route("/", get(handle_root))
1674            .fallback(|_req: Request<Body>| async { (StatusCode::FOUND, "redirected") })
1675            .with_state::<()>(())
1676            .compile()
1677            .expect("compile");
1678        let resp = router.handle_request(make_req("GET", "/missing")).await;
1679        assert_eq!(resp.status(), StatusCode::FOUND);
1680    }
1681
1682    // ── route deduplication ───────────────────────────────────────────────────
1683
1684    #[tokio::test]
1685    async fn test_overlapping_method_route_panics() {
1686        async fn v1() -> &'static str {
1687            "v1"
1688        }
1689        async fn v2() -> &'static str {
1690            "v2"
1691        }
1692
1693        // Registering the same method for the same path twice panics,
1694        // matching Axum's "Overlapping method route" panic.
1695        let result = std::panic::catch_unwind(|| {
1696            Router::<()>::new()
1697                .route("/dup", get(v1))
1698                .route("/dup", get(v2))
1699        });
1700        assert!(result.is_err());
1701    }
1702
1703    #[tokio::test]
1704    async fn test_non_overlapping_methods_on_same_path_merge() {
1705        async fn handle_get() -> &'static str {
1706            "got"
1707        }
1708        async fn handle_post() -> &'static str {
1709            "posted"
1710        }
1711
1712        // Registering different methods for the same path across separate
1713        // `.route()` calls merges into a single route answering both —
1714        // matching Axum, not tachyon-web's previous "any repeat path errors"
1715        // behavior.
1716        let app = Router::new()
1717            .route("/x", get(handle_get))
1718            .route("/x", post(handle_post))
1719            .with_state::<()>(())
1720            .compile()
1721            .expect("compile");
1722
1723        let get_resp = app.handle_request(make_req("GET", "/x")).await;
1724        assert_eq!(get_resp.status(), StatusCode::OK);
1725        let get_body = http_body_util::BodyExt::collect(get_resp.into_body())
1726            .await
1727            .unwrap()
1728            .to_bytes();
1729        assert_eq!(&get_body[..], b"got");
1730
1731        let post_resp = app.handle_request(make_req("POST", "/x")).await;
1732        assert_eq!(post_resp.status(), StatusCode::OK);
1733        let post_body = http_body_util::BodyExt::collect(post_resp.into_body())
1734            .await
1735            .unwrap()
1736            .to_bytes();
1737        assert_eq!(&post_body[..], b"posted");
1738    }
1739
1740    // ── normalize_route_pattern ───────────────────────────────────────────────
1741
1742    #[test]
1743    fn test_normalize_no_change() {
1744        assert_eq!(normalize_route_pattern("/static/path"), "/static/path");
1745    }
1746
1747    #[test]
1748    fn test_normalize_colon_param() {
1749        assert_eq!(normalize_route_pattern("/user/:id"), "/user/{id}");
1750    }
1751
1752    #[test]
1753    fn test_normalize_wildcard() {
1754        assert_eq!(normalize_route_pattern("/files/*path"), "/files/{*path}");
1755    }
1756
1757    #[test]
1758    fn test_normalize_mixed() {
1759        assert_eq!(
1760            normalize_route_pattern("/api/:version/files/*rest"),
1761            "/api/{version}/files/{*rest}"
1762        );
1763    }
1764
1765    // ── nested routers ────────────────────────────────────────────────────────
1766
1767    #[tokio::test]
1768    async fn test_nested_router() {
1769        let api = Router::new().route("/status", get(handle_root));
1770        let app = Router::new()
1771            .nest("/api/v1", api)
1772            .with_state::<()>(())
1773            .compile()
1774            .expect("compile");
1775
1776        let resp = app.handle_request(make_req("GET", "/api/v1/status")).await;
1777        assert_eq!(resp.status(), StatusCode::OK);
1778    }
1779
1780    #[tokio::test]
1781    async fn test_nested_router_root() {
1782        let api = Router::new().route("/", get(handle_root));
1783        let app = Router::new()
1784            .nest("/api", api)
1785            .with_state::<()>(())
1786            .compile()
1787            .expect("compile");
1788
1789        let resp = app.handle_request(make_req("GET", "/api")).await;
1790        assert_eq!(resp.status(), StatusCode::OK);
1791    }
1792
1793    // ── nest() URI stripping (Axum parity) ───────────────────────────────────
1794
1795    #[tokio::test]
1796    async fn test_nest_strips_prefix_from_uri() {
1797        use hyper::Uri;
1798
1799        async fn echo_uri(uri: Uri) -> String {
1800            uri.path().to_string()
1801        }
1802
1803        let api = Router::new().route("/users/{id}", get(echo_uri));
1804        let app = Router::new()
1805            .nest("/api", api)
1806            .with_state::<()>(())
1807            .compile()
1808            .expect("compile");
1809
1810        let resp = app.handle_request(make_req("GET", "/api/users/42")).await;
1811        assert_eq!(resp.status(), StatusCode::OK);
1812        let body = http_body_util::BodyExt::collect(resp.into_body())
1813            .await
1814            .unwrap()
1815            .to_bytes();
1816        // Matches Axum: the nested handler sees the prefix-stripped path.
1817        assert_eq!(&body[..], b"/users/42");
1818    }
1819
1820    #[cfg(feature = "original-uri")]
1821    #[tokio::test]
1822    async fn test_nest_original_uri_preserves_full_path() {
1823        use crate::routing::extract::OriginalUri;
1824
1825        async fn echo_original(OriginalUri(uri): OriginalUri) -> String {
1826            uri.path().to_string()
1827        }
1828
1829        let api = Router::new().route("/users/{id}", get(echo_original));
1830        let app = Router::new()
1831            .nest("/api", api)
1832            .with_state::<()>(())
1833            .compile()
1834            .expect("compile");
1835
1836        let resp = app.handle_request(make_req("GET", "/api/users/42")).await;
1837        assert_eq!(resp.status(), StatusCode::OK);
1838        let body = http_body_util::BodyExt::collect(resp.into_body())
1839            .await
1840            .unwrap()
1841            .to_bytes();
1842        // OriginalUri recovers the full, pre-strip path.
1843        assert_eq!(&body[..], b"/api/users/42");
1844    }
1845
1846    #[tokio::test]
1847    async fn test_nest_two_levels_accumulates_prefix() {
1848        async fn echo_uri(uri: hyper::Uri) -> String {
1849            uri.path().to_string()
1850        }
1851
1852        let innermost = Router::new().route("/users", get(echo_uri));
1853        let v1 = Router::new().nest("/v1", innermost);
1854        let app = Router::new()
1855            .nest("/api", v1)
1856            .with_state::<()>(())
1857            .compile()
1858            .expect("compile");
1859
1860        let resp = app.handle_request(make_req("GET", "/api/v1/users")).await;
1861        assert_eq!(resp.status(), StatusCode::OK);
1862        let body = http_body_util::BodyExt::collect(resp.into_body())
1863            .await
1864            .unwrap()
1865            .to_bytes();
1866        assert_eq!(&body[..], b"/users");
1867    }
1868
1869    #[tokio::test]
1870    async fn test_non_nested_route_uri_unaffected() {
1871        // A route registered directly (no `.nest()`) must see its Uri untouched.
1872        async fn echo_uri(uri: hyper::Uri) -> String {
1873            uri.path().to_string()
1874        }
1875        let app = Router::new()
1876            .route("/users/{id}", get(echo_uri))
1877            .with_state::<()>(())
1878            .compile()
1879            .expect("compile");
1880
1881        let resp = app.handle_request(make_req("GET", "/users/7")).await;
1882        let body = http_body_util::BodyExt::collect(resp.into_body())
1883            .await
1884            .unwrap()
1885            .to_bytes();
1886        assert_eq!(&body[..], b"/users/7");
1887    }
1888
1889    // ── MatchedPath ───────────────────────────────────────────────────────────
1890
1891    #[cfg(feature = "matched-path")]
1892    #[tokio::test]
1893    async fn test_matched_path_returns_route_pattern() {
1894        use crate::routing::extract::MatchedPath;
1895
1896        async fn handler(path: MatchedPath) -> String {
1897            path.as_str().to_string()
1898        }
1899
1900        let app = Router::new()
1901            .route("/users/{id}", get(handler))
1902            .with_state::<()>(())
1903            .compile()
1904            .expect("compile");
1905
1906        let resp = app.handle_request(make_req("GET", "/users/99")).await;
1907        assert_eq!(resp.status(), StatusCode::OK);
1908        let body = http_body_util::BodyExt::collect(resp.into_body())
1909            .await
1910            .unwrap()
1911            .to_bytes();
1912        assert_eq!(&body[..], b"/users/{id}");
1913    }
1914
1915    #[cfg(feature = "matched-path")]
1916    #[tokio::test]
1917    async fn test_matched_path_missing_returns_500() {
1918        use crate::routing::extract::{FromRequestParts, MatchedPath};
1919        // Directly exercising the extractor without going through the router at
1920        // all (no `MatchedPath` extension present) must reject with 500.
1921        let mut parts = Request::builder().uri("/").body(()).unwrap().into_parts().0;
1922        let res = MatchedPath::from_request_parts(&mut parts, &());
1923        assert!(res.is_err());
1924    }
1925
1926    // ── any() / CONNECT ───────────────────────────────────────────────────────
1927
1928    #[tokio::test]
1929    async fn test_any_dispatches_every_method() {
1930        async fn handler() -> &'static str {
1931            "any"
1932        }
1933        let app = Router::new()
1934            .route("/x", any(handler))
1935            .with_state::<()>(())
1936            .compile()
1937            .expect("compile");
1938
1939        for method in [
1940            "GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE",
1941        ] {
1942            let resp = app.handle_request(make_req(method, "/x")).await;
1943            assert_eq!(resp.status(), StatusCode::OK, "method: {method}");
1944        }
1945    }
1946
1947    #[tokio::test]
1948    async fn test_connect_route() {
1949        async fn handler() -> &'static str {
1950            "connected"
1951        }
1952        let app = Router::new()
1953            .route("/tunnel", connect(handler))
1954            .with_state::<()>(())
1955            .compile()
1956            .expect("compile");
1957
1958        let resp = app.handle_request(make_req("CONNECT", "/tunnel")).await;
1959        assert_eq!(resp.status(), StatusCode::OK);
1960    }
1961
1962    #[tokio::test]
1963    async fn test_extra_routing_features() {
1964        async fn dummy() -> &'static str {
1965            "ok"
1966        }
1967
1968        // 1. method_index with other methods
1969        assert_eq!(method_index(&Method::OPTIONS), Some(4));
1970        assert_eq!(method_index(&Method::HEAD), Some(5));
1971        assert_eq!(method_index(&Method::TRACE), Some(7));
1972        assert_eq!(method_index(&Method::CONNECT), Some(8));
1973        assert_eq!(method_index(&Method::PATCH), Some(6));
1974
1975        // 2. MethodRouter debug and default
1976        let mr = MethodRouter::<()>::default();
1977        let dbg = format!("{mr:?}");
1978        assert!(dbg.contains("MethodRouter"));
1979
1980        // 3. MethodRouter shortcuts & helpers
1981        let _mr2 = MethodRouter::<()>::new()
1982            .options(dummy)
1983            .head(dummy)
1984            .trace(dummy)
1985            .put(dummy)
1986            .delete(dummy)
1987            .patch(dummy);
1988
1989        let _mr3 = put::<_, _, ()>(dummy);
1990        let _mr4 = delete::<_, _, ()>(dummy);
1991        let _mr5 = patch::<_, _, ()>(dummy);
1992
1993        // 4. Router debug, default, and RouterError display/from
1994        let r = Router::<()>::default();
1995        let r_dbg = format!("{r:?}");
1996        assert!(r_dbg.contains("Router"));
1997
1998        let compiled = r.compile().unwrap();
1999        let cr_dbg = format!("{compiled:?}");
2000        assert!(cr_dbg.contains("CompiledRouter"));
2001
2002        let dup_err = RouterError::DuplicateRoute("foo".to_string());
2003        assert!(dup_err.to_string().contains("Duplicate route"));
2004
2005        // Trigger matchit InsertError
2006        let bad_router = Router::new()
2007            .route("/user/:id", get(dummy))
2008            .route("/user/*path", get(dummy))
2009            .with_state::<()>(())
2010            .compile();
2011        assert!(bad_router.is_err());
2012        let insert_err = bad_router.unwrap_err();
2013        assert!(insert_err.to_string().contains("Router insert error"));
2014
2015        // 5. serve_static
2016        let dir = tempfile::tempdir().unwrap();
2017        std::fs::write(dir.path().join("index.html"), "hello static").unwrap();
2018        let app_static = Router::new()
2019            .serve_static(dir.path())
2020            .with_state::<()>(())
2021            .compile()
2022            .unwrap();
2023        let resp = app_static.handle_request(make_req("GET", "/")).await;
2024        assert_eq!(resp.status(), StatusCode::OK);
2025
2026        // 6. serve_file_dynamic
2027        let file_path = dir.path().join("dynamic.txt");
2028        std::fs::write(&file_path, "hello dynamic").unwrap();
2029        let app_dynamic = Router::new()
2030            .serve_file_dynamic("/dyn", file_path.to_str().unwrap())
2031            .with_state::<()>(())
2032            .compile()
2033            .unwrap();
2034
2035        let resp_dyn = app_dynamic.handle_request(make_req("GET", "/dyn")).await;
2036        assert_eq!(resp_dyn.status(), StatusCode::OK);
2037
2038        // serve_file_dynamic error path
2039        let app_dyn_err = Router::new()
2040            .serve_file_dynamic("/dyn_err", "/nonexistent/file")
2041            .with_state::<()>(())
2042            .compile()
2043            .unwrap();
2044        let resp_dyn_err = app_dyn_err
2045            .handle_request(make_req("GET", "/dyn_err"))
2046            .await;
2047        assert_eq!(resp_dyn_err.status(), StatusCode::NOT_FOUND);
2048
2049        // 7. Nest empty nested_path
2050        let sub = Router::new().route("/", get(dummy));
2051        let nested_empty = Router::new()
2052            .nest("", sub)
2053            .with_state::<()>(())
2054            .compile()
2055            .unwrap();
2056        let resp_nested = nested_empty.handle_request(make_req("GET", "/")).await;
2057        assert_eq!(resp_nested.status(), StatusCode::OK);
2058
2059        // 8. Router merge
2060        let r1 = Router::new().route("/r1", get(dummy));
2061        let r2 = Router::new().route("/r2", get(dummy));
2062        let merged = r1.merge(r2).with_state::<()>(()).compile().unwrap();
2063        assert_eq!(
2064            merged.handle_request(make_req("GET", "/r1")).await.status(),
2065            StatusCode::OK
2066        );
2067        assert_eq!(
2068            merged.handle_request(make_req("GET", "/r2")).await.status(),
2069            StatusCode::OK
2070        );
2071    }
2072
2073    #[tokio::test]
2074    async fn test_merge_adopts_the_one_fallback_present() {
2075        async fn h() -> &'static str {
2076            "h"
2077        }
2078        async fn fb() -> &'static str {
2079            "merged-fallback"
2080        }
2081
2082        let r1 = Router::new().route("/r1", get(h));
2083        let r2 = Router::new().route("/r2", get(h)).fallback(fb);
2084        let merged = r1.merge(r2).with_state::<()>(()).compile().unwrap();
2085
2086        let resp = merged.handle_request(make_req("GET", "/missing")).await;
2087        assert_eq!(resp.status(), StatusCode::OK);
2088        let body = http_body_util::BodyExt::collect(resp.into_body())
2089            .await
2090            .unwrap()
2091            .to_bytes();
2092        assert_eq!(&body[..], b"merged-fallback");
2093    }
2094
2095    #[test]
2096    fn test_merge_two_fallbacks_panics() {
2097        async fn fb1() -> &'static str {
2098            "fb1"
2099        }
2100        async fn fb2() -> &'static str {
2101            "fb2"
2102        }
2103
2104        let result = std::panic::catch_unwind(|| {
2105            let r1 = Router::<()>::new().fallback(fb1);
2106            let r2 = Router::<()>::new().fallback(fb2);
2107            r1.merge(r2)
2108        });
2109        assert!(result.is_err());
2110    }
2111
2112    // ── MethodRouter::hoop() (single-middleware shorthand) ──────────────────────
2113
2114    #[tokio::test]
2115    async fn test_method_router_hoop_installs_middleware() {
2116        async fn handler() -> &'static str {
2117            "hi"
2118        }
2119        async fn tag_response(req: Request<Body>, next: middleware::Next<()>) -> Response<Body> {
2120            let mut resp = next.run(req).await;
2121            let _ = resp.headers_mut().insert(
2122                hyper::header::HeaderName::from_static("x-mr-hoop"),
2123                hyper::header::HeaderValue::from_static("yes"),
2124            );
2125            resp
2126        }
2127
2128        // `.hoop()` on a bare `MethodRouter` — distinct from `Router::hoop`,
2129        // which never calls through to `MethodRouter::hoop`; it calls
2130        // `MethodRouter::hoop_at` directly on every registered route instead.
2131        let mr = get(handler).hoop(tag_response);
2132        let app = Router::new()
2133            .route("/x", mr)
2134            .with_state::<()>(())
2135            .compile()
2136            .expect("compile");
2137
2138        let resp = app.handle_request(make_req("GET", "/x")).await;
2139        assert_eq!(resp.status(), StatusCode::OK);
2140        assert_eq!(resp.headers().get("x-mr-hoop").expect("header set"), "yes");
2141    }
2142
2143    // ── options()/head()/trace() free functions ──────────────────────────────────
2144
2145    #[tokio::test]
2146    async fn test_options_head_trace_free_functions() {
2147        async fn handle_options() -> &'static str {
2148            "opts"
2149        }
2150        async fn handle_trace() -> &'static str {
2151            "trace"
2152        }
2153        async fn handle_head() -> Response<Body> {
2154            Response::builder()
2155                .header("x-handler", "head")
2156                .body(Body::full(Bytes::from_static(b"head-only")))
2157                .unwrap_or_else(|_| Response::new(Body::empty()))
2158        }
2159        async fn handle_get_for_head() -> Response<Body> {
2160            Response::builder()
2161                .header("x-handler", "get")
2162                .body(Body::full(Bytes::from_static(b"get-for-head")))
2163                .unwrap_or_else(|_| Response::new(Body::empty()))
2164        }
2165        async fn handle_get_only() -> &'static str {
2166            "get-only"
2167        }
2168
2169        let app = Router::new()
2170            .route("/opts", options(handle_options))
2171            .route("/tracer", trace(handle_trace))
2172            // An explicit HEAD handler must take priority over the implicit
2173            // GET fallback (see `CompiledRouter::handle_request`'s
2174            // `falls_back_to_get` logic).
2175            .route("/headroute", head(handle_head).get(handle_get_for_head))
2176            // No explicit HEAD handler here, so HEAD must still fall back to GET.
2177            .route("/getonly", get(handle_get_only))
2178            .with_state::<()>(())
2179            .compile()
2180            .expect("compile");
2181
2182        let resp = app.handle_request(make_req("OPTIONS", "/opts")).await;
2183        assert_eq!(resp.status(), StatusCode::OK);
2184        let body = http_body_util::BodyExt::collect(resp.into_body())
2185            .await
2186            .unwrap()
2187            .to_bytes();
2188        assert_eq!(&body[..], b"opts");
2189
2190        let resp = app.handle_request(make_req("TRACE", "/tracer")).await;
2191        assert_eq!(resp.status(), StatusCode::OK);
2192        let body = http_body_util::BodyExt::collect(resp.into_body())
2193            .await
2194            .unwrap()
2195            .to_bytes();
2196        assert_eq!(&body[..], b"trace");
2197
2198        // The explicit HEAD handler must be the one that actually runs.
2199        let resp = app.handle_request(make_req("HEAD", "/headroute")).await;
2200        assert_eq!(resp.status(), StatusCode::OK);
2201        assert_eq!(
2202            resp.headers()
2203                .get("x-handler")
2204                .map(hyper::header::HeaderValue::as_bytes),
2205            Some(&b"head"[..])
2206        );
2207        let body = http_body_util::BodyExt::collect(resp.into_body())
2208            .await
2209            .unwrap()
2210            .to_bytes();
2211        assert!(body.is_empty(), "HEAD responses must have an empty body");
2212
2213        // GET on the same route still goes to the GET handler.
2214        let get_resp = app.handle_request(make_req("GET", "/headroute")).await;
2215        assert_eq!(
2216            get_resp
2217                .headers()
2218                .get("x-handler")
2219                .map(hyper::header::HeaderValue::as_bytes),
2220            Some(&b"get"[..])
2221        );
2222        let get_body = http_body_util::BodyExt::collect(get_resp.into_body())
2223            .await
2224            .unwrap()
2225            .to_bytes();
2226        assert_eq!(&get_body[..], b"get-for-head");
2227
2228        // No HEAD handler registered: HEAD must fall back to the GET handler.
2229        let resp = app.handle_request(make_req("HEAD", "/getonly")).await;
2230        assert_eq!(resp.status(), StatusCode::OK);
2231        let body = http_body_util::BodyExt::collect(resp.into_body())
2232            .await
2233            .unwrap()
2234            .to_bytes();
2235        assert!(body.is_empty(), "HEAD responses must have an empty body");
2236    }
2237
2238    // ── into_make_service() (Axum API-parity no-op) ──────────────────────────────
2239
2240    #[tokio::test]
2241    async fn test_into_make_service_returns_usable_router() {
2242        async fn handler() -> &'static str {
2243            "ims"
2244        }
2245
2246        let app = Router::new()
2247            .route("/x", get(handler))
2248            .into_make_service()
2249            .with_state::<()>(())
2250            .compile()
2251            .expect("compile");
2252
2253        let resp = app.handle_request(make_req("GET", "/x")).await;
2254        assert_eq!(resp.status(), StatusCode::OK);
2255    }
2256
2257    // ── method_not_allowed_fallback ──────────────────────────────────────────────
2258
2259    #[tokio::test]
2260    async fn test_method_not_allowed_fallback_overrides_default_405() {
2261        async fn get_handler() -> &'static str {
2262            "got"
2263        }
2264        async fn custom_405() -> (StatusCode, &'static str) {
2265            (StatusCode::IM_A_TEAPOT, "custom-405")
2266        }
2267
2268        let app = Router::new()
2269            .route("/x", get(get_handler))
2270            .method_not_allowed_fallback(custom_405)
2271            .with_state::<()>(())
2272            .compile()
2273            .expect("compile");
2274
2275        // /x exists but has no POST handler — the custom fallback answers
2276        // instead of the default bare 405.
2277        let resp = app.handle_request(make_req("POST", "/x")).await;
2278        assert_eq!(resp.status(), StatusCode::IM_A_TEAPOT);
2279        let body = http_body_util::BodyExt::collect(resp.into_body())
2280            .await
2281            .unwrap()
2282            .to_bytes();
2283        assert_eq!(&body[..], b"custom-405");
2284    }
2285
2286    // ── hoop()/hoop_at() wrapping the fallback & method_not_allowed_fallback ────
2287
2288    #[tokio::test]
2289    async fn test_hoop_wraps_fallback_and_method_not_allowed_fallback() {
2290        async fn get_handler() -> &'static str {
2291            "got"
2292        }
2293        async fn custom_fallback() -> &'static str {
2294            "custom-fallback"
2295        }
2296        async fn custom_405() -> &'static str {
2297            "custom-405"
2298        }
2299        async fn tag_response(req: Request<Body>, next: middleware::Next<()>) -> Response<Body> {
2300            let mut resp = next.run(req).await;
2301            let _ = resp.headers_mut().insert(
2302                hyper::header::HeaderName::from_static("x-hoop"),
2303                hyper::header::HeaderValue::from_static("wrapped"),
2304            );
2305            resp
2306        }
2307
2308        // `.fallback()`/`.method_not_allowed_fallback()` must be set *before*
2309        // `.hoop()`, since `Router::hoop_at` only wraps whichever of the two
2310        // is already installed at the time it runs.
2311        let app = Router::new()
2312            .route("/x", get(get_handler))
2313            .fallback(custom_fallback)
2314            .method_not_allowed_fallback(custom_405)
2315            .hoop(tag_response)
2316            .with_state::<()>(())
2317            .compile()
2318            .expect("compile");
2319
2320        // Middleware still runs around a normally-matched route.
2321        let resp = app.handle_request(make_req("GET", "/x")).await;
2322        assert_eq!(resp.status(), StatusCode::OK);
2323        assert_eq!(
2324            resp.headers().get("x-hoop").expect("wraps route"),
2325            "wrapped"
2326        );
2327
2328        // Middleware still runs around the custom fallback (unmatched path).
2329        let resp = app.handle_request(make_req("GET", "/missing")).await;
2330        assert_eq!(resp.status(), StatusCode::OK);
2331        assert_eq!(
2332            resp.headers().get("x-hoop").expect("wraps fallback"),
2333            "wrapped"
2334        );
2335        let body = http_body_util::BodyExt::collect(resp.into_body())
2336            .await
2337            .unwrap()
2338            .to_bytes();
2339        assert_eq!(&body[..], b"custom-fallback");
2340
2341        // Middleware still runs around the method-not-allowed fallback too.
2342        let resp = app.handle_request(make_req("POST", "/x")).await;
2343        assert_eq!(resp.status(), StatusCode::OK);
2344        assert_eq!(
2345            resp.headers().get("x-hoop").expect("wraps 405 fallback"),
2346            "wrapped"
2347        );
2348        let body = http_body_util::BodyExt::collect(resp.into_body())
2349            .await
2350            .unwrap()
2351            .to_bytes();
2352        assert_eq!(&body[..], b"custom-405");
2353    }
2354
2355    // ── RouterError::DuplicateRoute ───────────────────────────────────────────────
2356
2357    #[test]
2358    fn test_compile_returns_duplicate_route_err() {
2359        async fn handler() -> &'static str {
2360            "dup"
2361        }
2362
2363        // `Router::route`/`.nest()`/`.merge()` all merge same-path entries via
2364        // `push_or_merge_route`, so two *identical* path strings can never
2365        // reach `compile()`'s `routes` `Vec` through the public builder API —
2366        // see the doc comment on `RouterError::DuplicateRoute`. Constructing
2367        // the `Router` directly is the only way to exercise this internal
2368        // invariant check; this test lives inside the `routing` module tree,
2369        // so it can see the otherwise-private `routes` field to do that.
2370        let router = Router::<()> {
2371            routes: vec![
2372                ("/dup".to_string(), get(handler)),
2373                ("/dup".to_string(), get(handler)),
2374            ],
2375            fallback: None,
2376            method_not_allowed_fallback: None,
2377            state: None,
2378            normalize_trailing_slash: false,
2379            #[cfg(feature = "tower")]
2380            compiled: None,
2381        };
2382
2383        let result = router.compile();
2384        assert!(matches!(result, Err(RouterError::DuplicateRoute(ref p)) if p == "/dup"));
2385    }
2386
2387    // ── nest() query-string preservation ──────────────────────────────────────────
2388
2389    #[tokio::test]
2390    async fn test_nest_strips_prefix_preserves_query_string() {
2391        async fn echo_full(uri: hyper::Uri) -> String {
2392            uri.query()
2393                .map_or_else(|| uri.path().to_string(), |q| format!("{}?{q}", uri.path()))
2394        }
2395
2396        let api = Router::new().route("/users/{id}", get(echo_full));
2397        let app = Router::new()
2398            .nest("/api", api)
2399            .with_state::<()>(())
2400            .compile()
2401            .expect("compile");
2402
2403        let resp = app
2404            .handle_request(make_req("GET", "/api/users/42?active=true"))
2405            .await;
2406        assert_eq!(resp.status(), StatusCode::OK);
2407        let body = http_body_util::BodyExt::collect(resp.into_body())
2408            .await
2409            .unwrap()
2410            .to_bytes();
2411        assert_eq!(&body[..], b"/users/42?active=true");
2412    }
2413
2414    // ── normalize_trailing_slash ────────────────────────────────────────────────
2415
2416    #[tokio::test]
2417    async fn test_normalize_trailing_slash_preserves_query_string() {
2418        async fn echo_full(uri: hyper::Uri) -> String {
2419            uri.query()
2420                .map_or_else(|| uri.path().to_string(), |q| format!("{}?{q}", uri.path()))
2421        }
2422
2423        let app = Router::new()
2424            .route("/about", get(echo_full))
2425            .normalize_trailing_slash()
2426            .with_state::<()>(())
2427            .compile()
2428            .expect("compile");
2429
2430        let resp = app.handle_request(make_req("GET", "/about/?x=1")).await;
2431        assert_eq!(resp.status(), StatusCode::OK);
2432        let body = http_body_util::BodyExt::collect(resp.into_body())
2433            .await
2434            .unwrap()
2435            .to_bytes();
2436        assert_eq!(&body[..], b"/about?x=1");
2437    }
2438
2439    #[tokio::test]
2440    async fn test_normalize_trailing_slash_no_trailing_slash_is_untouched() {
2441        async fn handler() -> &'static str {
2442            "no-trailing"
2443        }
2444
2445        let app = Router::new()
2446            .route("/about", get(handler))
2447            .normalize_trailing_slash()
2448            .with_state::<()>(())
2449            .compile()
2450            .expect("compile");
2451
2452        // Already has no trailing slash → `strip_trailing_slash`'s
2453        // early-return branch.
2454        let resp = app.handle_request(make_req("GET", "/about")).await;
2455        assert_eq!(resp.status(), StatusCode::OK);
2456
2457        // Root `/` is length 1 → also hits the early-return branch, so it's
2458        // never stripped down to an empty (invalid) path.
2459        let root_app = Router::new()
2460            .route("/", get(handler))
2461            .normalize_trailing_slash()
2462            .with_state::<()>(())
2463            .compile()
2464            .expect("compile");
2465        let resp = root_app.handle_request(make_req("GET", "/")).await;
2466        assert_eq!(resp.status(), StatusCode::OK);
2467    }
2468}