Skip to main content

churust_core/
router.rs

1//! The trie-based [`Router`], its [`Match`] result, and the [`RouteBuilder`]
2//! DSL used inside [`AppBuilder::routing`](crate::AppBuilder::routing).
3//!
4//! Routes are matched segment by segment against static text, `{param}`
5//! captures, and a trailing `{name...}` wildcard. Static segments win over
6//! parameters, which win over wildcards.
7
8use crate::handler::{boxed, BoxHandler, IntoHandler};
9use http::Method;
10use std::collections::HashMap;
11
12/// The outcome of routing a `(method, path)` pair against the [`Router`].
13///
14/// Returned by [`Router::route`]. The framework translates each variant into a
15/// response: [`Found`](Match::Found) runs the handler,
16/// [`MethodNotAllowed`](Match::MethodNotAllowed) yields `405` with an `Allow`
17/// header, and [`NotFound`](Match::NotFound) yields `404`.
18///
19/// ```
20/// use churust_core::{boxed, Call, IntoHandler, Router, Match};
21/// use http::Method;
22///
23/// let mut router = Router::new();
24/// router.add(Method::GET, "/users/{id}", boxed((|_c: Call| async { "ok" }).into_handler()));
25///
26/// match router.route(&Method::GET, "/users/7") {
27///     Match::Found { params, .. } => assert_eq!(params.get("id").unwrap(), "7"),
28///     _ => panic!("expected a match"),
29/// }
30/// assert!(matches!(router.route(&Method::GET, "/nope"), Match::NotFound));
31/// assert!(matches!(router.route(&Method::POST, "/users/7"), Match::MethodNotAllowed { .. }));
32/// ```
33///
34/// Marked `#[non_exhaustive]`: a `match` on this enum outside `churust-core`
35/// must carry a `_` arm, so that a future routing outcome is not a breaking
36/// change.
37#[non_exhaustive]
38pub enum Match {
39    /// A handler matched the path and method. Carries the matched handler and
40    /// the captured path parameters (`{name}` -> value).
41    Found {
42        /// The handler registered for this `(path, method)`.
43        handler: BoxHandler,
44        /// The captured path parameters, keyed by name.
45        params: HashMap<String, String>,
46    },
47    /// The path matched a route, but not for this method. `allow` lists the
48    /// methods that *are* registered (used to build the `Allow` header).
49    MethodNotAllowed {
50        /// The methods registered for the matched path.
51        allow: Vec<Method>,
52    },
53    /// No route matched the path at all.
54    NotFound,
55    /// A path segment could not be percent-decoded — a malformed escape or
56    /// bytes that are not valid UTF-8. The dispatcher turns this into
57    /// `400 Bad Request`.
58    BadPath,
59}
60
61#[derive(Default)]
62struct Node {
63    statics: HashMap<String, Node>,
64    param: Option<(String, Box<Node>)>,      // {name}
65    wildcard: Option<(String, BoxHandlers)>, // {name...} terminal
66    handlers: BoxHandlers,
67}
68
69#[derive(Default)]
70struct BoxHandlers(HashMap<Method, BoxHandler>);
71
72impl std::fmt::Debug for Node {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        f.debug_struct("Node").finish_non_exhaustive()
75    }
76}
77
78/// A compiled, trie-based router mapping `(method, path)` to a handler.
79///
80/// Build one with [`Router::new`], register routes with [`Router::add`], and
81/// look them up with [`Router::route`]. Inside an application you usually do not
82/// touch the `Router` directly — the
83/// [`RouteBuilder`] DSL in
84/// [`AppBuilder::routing`](crate::AppBuilder::routing) populates it for you.
85///
86/// Supported pattern syntax (full paths, leading `/`):
87/// - static segments: `/users/list`
88/// - named parameters: `/users/{id}` (captured as `id`)
89/// - trailing wildcard: `/files/{path...}` (captures the remaining path,
90///   slashes included; must be the last segment)
91///
92/// ```
93/// use churust_core::{boxed, Call, IntoHandler, Router, Match};
94/// use http::Method;
95///
96/// let mut router = Router::new();
97/// router.add(Method::GET, "/files/{path...}", boxed((|_c: Call| async { "" }).into_handler()));
98/// match router.route(&Method::GET, "/files/a/b/c.txt") {
99///     Match::Found { params, .. } => assert_eq!(params.get("path").unwrap(), "a/b/c.txt"),
100///     _ => panic!("expected wildcard match"),
101/// }
102/// ```
103#[derive(Debug, Default)]
104pub struct Router {
105    root: Node,
106}
107
108impl Router {
109    /// Create an empty router. Equivalent to [`Router::default`].
110    pub fn new() -> Self {
111        Self::default()
112    }
113
114    /// Insert `handler` for `method` at `pattern` (the full path, e.g.
115    /// `/users/{id}`).
116    ///
117    /// Registering different methods at the same path is fine; registering the
118    /// same `(method, path)` twice replaces the earlier handler.
119    ///
120    /// # Panics
121    ///
122    /// Panics if a `{name...}` wildcard segment is not the final segment of the
123    /// pattern.
124    ///
125    /// ```
126    /// use churust_core::{boxed, Call, IntoHandler, Router, Match};
127    /// use http::Method;
128    ///
129    /// let mut router = Router::new();
130    /// router.add(Method::GET, "/ping", boxed((|_c: Call| async { "pong" }).into_handler()));
131    /// assert!(matches!(router.route(&Method::GET, "/ping"), Match::Found { .. }));
132    /// ```
133    pub fn add(&mut self, method: Method, pattern: &str, handler: BoxHandler) {
134        let mut node = &mut self.root;
135        let segments: Vec<&str> = split_segments(pattern);
136        for (i, seg) in segments.iter().enumerate() {
137            if let Some(name) = seg.strip_prefix('{').and_then(|s| s.strip_suffix("...}")) {
138                // wildcard must be terminal
139                assert!(
140                    i == segments.len() - 1,
141                    "wildcard `{{{name}...}}` must be last segment"
142                );
143                let entry = node
144                    .wildcard
145                    .get_or_insert_with(|| (name.to_string(), BoxHandlers::default()));
146                entry.1 .0.insert(method, handler);
147                return;
148            } else if let Some(name) = seg.strip_prefix('{').and_then(|s| s.strip_suffix('}')) {
149                let entry = node
150                    .param
151                    .get_or_insert_with(|| (name.to_string(), Box::new(Node::default())));
152                node = entry.1.as_mut();
153            } else {
154                node = node.statics.entry(seg.to_string()).or_default();
155            }
156        }
157        node.handlers.0.insert(method, handler);
158    }
159
160    /// Route `path` for `method`, returning a [`Match`].
161    ///
162    /// Matching prefers static segments over `{param}` captures, and falls back
163    /// to a `{name...}` wildcard at the deepest matchable ancestor. A path that
164    /// matches a node with no handler for `method` yields
165    /// [`Match::MethodNotAllowed`]; a path that matches no node yields
166    /// [`Match::NotFound`].
167    ///
168    /// ```
169    /// use churust_core::{boxed, Call, IntoHandler, Router, Match};
170    /// use http::Method;
171    ///
172    /// let mut router = Router::new();
173    /// router.add(Method::GET, "/", boxed((|_c: Call| async { "home" }).into_handler()));
174    /// assert!(matches!(router.route(&Method::GET, "/"), Match::Found { .. }));
175    /// assert!(matches!(router.route(&Method::GET, "/missing"), Match::NotFound));
176    /// ```
177    pub fn route(&self, method: &Method, path: &str) -> Match {
178        // Split first, decode second. Decoding before splitting would let %2F
179        // manufacture separators and forge extra path segments.
180        let decoded = match decode_segments(path) {
181            Some(d) => d,
182            None => return Match::BadPath,
183        };
184        let segments: Vec<&str> = decoded.iter().map(String::as_str).collect();
185        let mut params = HashMap::new();
186
187        // 1. Exact walk. Static beats param, unchanged.
188        let exact = Self::walk(&self.root, &segments, 0, &mut params);
189        if let Some(node) = exact {
190            if let Some(h) = node.handlers.0.get(method) {
191                return Match::Found {
192                    handler: h.clone(),
193                    params,
194                };
195            }
196        }
197        let exact_allow: Vec<Method> = exact
198            .map(|n| n.handlers.0.keys().cloned().collect())
199            .unwrap_or_default();
200
201        // 2. Wildcard fallback. Reaching a node that has no handler for this
202        //    method is not the end of the search — a trailing `{name...}` at a
203        //    shallower depth may still serve the request. Without this, any
204        //    static route sharing a wildcard's prefix hides it entirely.
205        //
206        //    The walk above may have written captures into `params`. They
207        //    belong to the branch just abandoned and must not reach the
208        //    wildcard handler.
209        params.clear();
210        match Self::walk_wildcard(&self.root, &segments, 0, method, &mut params) {
211            Some(found @ Match::Found { .. }) => found,
212            Some(Match::MethodNotAllowed { allow: wild_allow }) => {
213                let mut allow = exact_allow;
214                for m in wild_allow {
215                    if !allow.contains(&m) {
216                        allow.push(m);
217                    }
218                }
219                Match::MethodNotAllowed { allow }
220            }
221            _ if !exact_allow.is_empty() => Match::MethodNotAllowed { allow: exact_allow },
222            _ => Match::NotFound,
223        }
224    }
225
226    /// The methods registered for `path`, including any a trailing wildcard
227    /// would serve. Empty when the path matches no route at all.
228    ///
229    /// Used by the dispatcher to build the `Allow` header for an `OPTIONS`
230    /// request that has no handler of its own.
231    ///
232    /// ```
233    /// use churust_core::{boxed, Call, IntoHandler, Router};
234    /// use http::Method;
235    ///
236    /// let mut router = Router::new();
237    /// router.add(Method::GET, "/x", boxed((|_c: Call| async { "" }).into_handler()));
238    /// assert_eq!(router.methods_for("/x"), vec![Method::GET]);
239    /// assert!(router.methods_for("/nope").is_empty());
240    /// ```
241    pub fn methods_for(&self, path: &str) -> Vec<Method> {
242        let decoded = match decode_segments(path) {
243            Some(d) => d,
244            None => return Vec::new(),
245        };
246        let segments: Vec<&str> = decoded.iter().map(String::as_str).collect();
247        let mut params = HashMap::new();
248        let mut out: Vec<Method> = Self::walk(&self.root, &segments, 0, &mut params)
249            .map(|n| n.handlers.0.keys().cloned().collect())
250            .unwrap_or_default();
251
252        // TRACE is a probe: nothing registers it, so `walk_wildcard` reports
253        // MethodNotAllowed carrying the wildcard's full method list.
254        params.clear();
255        if let Some(Match::MethodNotAllowed { allow }) =
256            Self::walk_wildcard(&self.root, &segments, 0, &Method::TRACE, &mut params)
257        {
258            for m in allow {
259                if !out.contains(&m) {
260                    out.push(m);
261                }
262            }
263        }
264        out
265    }
266
267    fn walk<'a>(
268        node: &'a Node,
269        segs: &[&str],
270        i: usize,
271        params: &mut HashMap<String, String>,
272    ) -> Option<&'a Node> {
273        if i == segs.len() {
274            return Some(node);
275        }
276        let seg = segs[i];
277        if let Some(child) = node.statics.get(seg) {
278            if let Some(n) = Self::walk(child, segs, i + 1, params) {
279                return Some(n);
280            }
281        }
282        if let Some((name, child)) = &node.param {
283            params.insert(name.clone(), seg.to_string());
284            if let Some(n) = Self::walk(child, segs, i + 1, params) {
285                return Some(n);
286            }
287            params.remove(name);
288        }
289        None
290    }
291
292    fn walk_wildcard(
293        node: &Node,
294        segs: &[&str],
295        i: usize,
296        method: &Method,
297        params: &mut HashMap<String, String>,
298    ) -> Option<Match> {
299        if let Some((name, handlers)) = &node.wildcard {
300            let rest = segs[i..].join("/");
301            params.insert(name.clone(), rest);
302            return Some(match handlers.0.get(method) {
303                Some(h) => Match::Found {
304                    handler: h.clone(),
305                    params: std::mem::take(params),
306                },
307                None => Match::MethodNotAllowed {
308                    allow: handlers.0.keys().cloned().collect(),
309                },
310            });
311        }
312        if i < segs.len() {
313            if let Some(child) = node.statics.get(segs[i]) {
314                if let Some(m) = Self::walk_wildcard(child, segs, i + 1, method, params) {
315                    return Some(m);
316                }
317            }
318            if let Some((pname, child)) = &node.param {
319                params.insert(pname.clone(), segs[i].to_string());
320                if let Some(m) = Self::walk_wildcard(child, segs, i + 1, method, params) {
321                    return Some(m);
322                }
323                params.remove(pname);
324            }
325        }
326        None
327    }
328}
329
330fn split_segments(path: &str) -> Vec<&str> {
331    path.split('/').filter(|s| !s.is_empty()).collect()
332}
333
334/// Split `path` on `/` and percent-decode each segment individually.
335///
336/// The order is the point. Decoding the whole path first would turn `%2F` into
337/// a separator and let a request forge segments it was never routed through.
338/// Returns `None` if any segment is undecodable, which becomes `400`.
339fn decode_segments(path: &str) -> Option<Vec<String>> {
340    split_segments(path)
341        .into_iter()
342        .map(crate::path::decode_path_segment)
343        .collect()
344}
345
346/// The route-definition DSL handed to the closure in
347/// [`AppBuilder::routing`](crate::AppBuilder::routing).
348///
349/// Register handlers with the per-method helpers ([`get`](RouteBuilder::get),
350/// [`post`](RouteBuilder::post), [`put`](RouteBuilder::put),
351/// [`delete`](RouteBuilder::delete)) or the generic [`method`](RouteBuilder::method).
352/// Group related routes under a common prefix with [`route`](RouteBuilder::route),
353/// which nests cleanly. Each handler may be an extractor closure or anything
354/// implementing [`Handler`](crate::Handler).
355///
356/// ```
357/// use churust_core::{Churust, Call, TestClient};
358/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
359/// let app = Churust::server()
360///     .routing(|r| {
361///         r.get("/", |_c: Call| async { "home" });
362///         r.route("/api", |r| {
363///             r.get("/health", |_c: Call| async { "ok" });
364///         });
365///     })
366///     .build();
367/// let res = TestClient::new(app).get("/api/health").send().await;
368/// assert_eq!(res.text(), "ok");
369/// # });
370/// ```
371pub struct RouteBuilder<'r> {
372    router: &'r mut Router,
373    prefix: String,
374}
375
376impl<'r> RouteBuilder<'r> {
377    pub(crate) fn new(router: &'r mut Router) -> Self {
378        Self {
379            router,
380            prefix: String::new(),
381        }
382    }
383
384    fn full(&self, path: &str) -> String {
385        let mut p = self.prefix.clone();
386        if !path.starts_with('/') {
387            p.push('/');
388        }
389        p.push_str(path);
390        p
391    }
392
393    /// Register a handler for `method` at `path`. Accepts both extractor
394    /// closures (via the `HandlerFn` family) and anything already implementing
395    /// `Handler` — including a pre-boxed `BoxHandler` — through `IntoHandler`.
396    pub fn method<Marker, H>(&mut self, method: Method, path: &str, handler: H) -> &mut Self
397    where
398        H: IntoHandler<Marker>,
399    {
400        let full = self.full(path);
401        self.router
402            .add(method, &full, boxed(handler.into_handler()));
403        self
404    }
405
406    /// Register a `GET` handler at `path`. Returns `&mut Self` for chaining.
407    pub fn get<Marker, H>(&mut self, path: &str, handler: H) -> &mut Self
408    where
409        H: IntoHandler<Marker>,
410    {
411        self.method(Method::GET, path, handler)
412    }
413    /// Register a `POST` handler at `path`. Returns `&mut Self` for chaining.
414    pub fn post<Marker, H>(&mut self, path: &str, handler: H) -> &mut Self
415    where
416        H: IntoHandler<Marker>,
417    {
418        self.method(Method::POST, path, handler)
419    }
420    /// Register a `PUT` handler at `path`. Returns `&mut Self` for chaining.
421    pub fn put<Marker, H>(&mut self, path: &str, handler: H) -> &mut Self
422    where
423        H: IntoHandler<Marker>,
424    {
425        self.method(Method::PUT, path, handler)
426    }
427    /// Register a `DELETE` handler at `path`. Returns `&mut Self` for chaining.
428    pub fn delete<Marker, H>(&mut self, path: &str, handler: H) -> &mut Self
429    where
430        H: IntoHandler<Marker>,
431    {
432        self.method(Method::DELETE, path, handler)
433    }
434
435    /// Open a nested scope: every route registered inside `f` has `path`
436    /// prepended to its pattern. Scopes nest arbitrarily, so prefixes compose.
437    /// Returns `&mut Self` for chaining.
438    pub fn route(&mut self, path: &str, f: impl FnOnce(&mut RouteBuilder)) -> &mut Self {
439        let prefix = self.full(path);
440        let mut child = RouteBuilder {
441            router: self.router,
442            prefix,
443        };
444        f(&mut child);
445        self
446    }
447}
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452    use crate::call::Call;
453    use bytes::Bytes;
454    use http::{HeaderMap, StatusCode, Uri};
455
456    fn build() -> Router {
457        let mut r = Router::new();
458        {
459            let mut b = RouteBuilder::new(&mut r);
460            b.get("/", |_c: Call| async { "root" });
461            b.route("/users", |b| {
462                b.get("/{id}", |c: Call| async move {
463                    format!("user {}", c.param_raw("id").unwrap())
464                });
465                b.post("/", |_c: Call| async { (StatusCode::CREATED, "created") });
466            });
467            b.get("/files/{path...}", |c: Call| async move {
468                format!("file {}", c.param_raw("path").unwrap())
469            });
470        }
471        r
472    }
473
474    fn run(r: &Router, m: Method, path: &str) -> Match {
475        r.route(&m, path)
476    }
477
478    #[tokio::test]
479    async fn matches_static_and_param() {
480        let r = build();
481        match run(&r, Method::GET, "/users/7") {
482            Match::Found { handler, params } => {
483                assert_eq!(params.get("id").unwrap(), "7");
484                let mut c = Call::new(
485                    Method::GET,
486                    "/users/7".parse::<Uri>().unwrap(),
487                    HeaderMap::new(),
488                    Bytes::new(),
489                );
490                c.set_params(params);
491                let res = handler.handle(c).await;
492                assert_eq!(res.body, Bytes::from("user 7"));
493            }
494            _ => panic!("expected Found"),
495        }
496    }
497
498    fn build_shadowed() -> Router {
499        let mut r = Router::new();
500        {
501            let mut b = RouteBuilder::new(&mut r);
502            b.get("/files/{path...}", |c: Call| async move {
503                format!("wild:{}", c.param_raw("path").unwrap_or(""))
504            });
505            b.get("/files/special/x", |_c: Call| async { "static" });
506            b.post("/files/only-post", |_c: Call| async { "posted" });
507        }
508        r
509    }
510
511    #[test]
512    fn path_params_are_percent_decoded() {
513        let mut r = Router::new();
514        {
515            let mut b = RouteBuilder::new(&mut r);
516            b.get("/u/{name}", |_c: Call| async { "" });
517        }
518        match r.route(&Method::GET, "/u/John%20Doe") {
519            Match::Found { params, .. } => assert_eq!(params.get("name").unwrap(), "John Doe"),
520            _ => panic!("expected a match"),
521        }
522    }
523
524    #[test]
525    fn encoded_slash_does_not_create_a_segment() {
526        let mut r = Router::new();
527        {
528            let mut b = RouteBuilder::new(&mut r);
529            b.get("/u/{name}", |_c: Call| async { "" });
530        }
531        // Matching at all proves %2F stayed inside one segment. Had it been
532        // decoded before splitting, this would be two segments and miss.
533        match r.route(&Method::GET, "/u/a%2Fb") {
534            Match::Found { params, .. } => assert_eq!(params.get("name").unwrap(), "a/b"),
535            _ => panic!("%2F must not manufacture a separator"),
536        }
537    }
538
539    #[test]
540    fn a_route_with_a_literal_space_is_reachable_encoded() {
541        let mut r = Router::new();
542        {
543            let mut b = RouteBuilder::new(&mut r);
544            b.get("/a b", |_c: Call| async { "" });
545        }
546        assert!(matches!(
547            r.route(&Method::GET, "/a%20b"),
548            Match::Found { .. }
549        ));
550    }
551
552    #[test]
553    fn malformed_encoding_is_a_bad_path() {
554        let mut r = Router::new();
555        {
556            let mut b = RouteBuilder::new(&mut r);
557            b.get("/u/{name}", |_c: Call| async { "" });
558        }
559        assert!(matches!(r.route(&Method::GET, "/u/%zz"), Match::BadPath));
560        assert!(matches!(r.route(&Method::GET, "/u/%FF"), Match::BadPath));
561    }
562
563    #[test]
564    fn wildcard_captures_decoded_segments() {
565        let mut r = Router::new();
566        {
567            let mut b = RouteBuilder::new(&mut r);
568            b.get("/f/{rest...}", |_c: Call| async { "" });
569        }
570        match r.route(&Method::GET, "/f/a%20b/c") {
571            Match::Found { params, .. } => assert_eq!(params.get("rest").unwrap(), "a b/c"),
572            _ => panic!("expected the wildcard to match"),
573        }
574    }
575
576    #[test]
577    fn wildcard_is_reachable_through_a_static_sibling() {
578        let r = build_shadowed();
579        match run(&r, Method::GET, "/files/special") {
580            Match::Found { params, .. } => {
581                assert_eq!(params.get("path").unwrap(), "special");
582            }
583            _ => panic!("wildcard should serve /files/special"),
584        }
585    }
586
587    #[test]
588    fn exact_match_still_wins_over_wildcard() {
589        let r = build_shadowed();
590        match run(&r, Method::GET, "/files/special/x") {
591            Match::Found { params, .. } => {
592                assert!(
593                    !params.contains_key("path"),
594                    "the static route captured a wildcard param"
595                );
596            }
597            _ => panic!("expected the static route"),
598        }
599    }
600
601    #[test]
602    fn allow_header_unions_exact_and_wildcard_methods() {
603        let r = build_shadowed();
604        match run(&r, Method::DELETE, "/files/only-post") {
605            Match::MethodNotAllowed { allow } => {
606                assert!(allow.contains(&Method::POST), "missing the exact method");
607                assert!(allow.contains(&Method::GET), "missing the wildcard method");
608            }
609            _ => panic!("expected 405"),
610        }
611    }
612
613    #[test]
614    fn abandoned_branch_params_do_not_leak_into_the_wildcard() {
615        let mut r = Router::new();
616        {
617            let mut b = RouteBuilder::new(&mut r);
618            // Matches /u/7/edit structurally, but has no GET handler.
619            b.post("/u/{id}/edit", |_c: Call| async { "edit" });
620            b.get("/u/{rest...}", |_c: Call| async { "wild" });
621        }
622        match r.route(&Method::GET, "/u/7/edit") {
623            Match::Found { params, .. } => {
624                assert_eq!(params.get("rest").unwrap(), "7/edit");
625                assert!(
626                    !params.contains_key("id"),
627                    "stale `id` leaked from the abandoned walk"
628                );
629            }
630            _ => panic!("the wildcard should have matched"),
631        }
632    }
633
634    #[test]
635    fn unknown_path_is_not_found() {
636        let r = build();
637        assert!(matches!(run(&r, Method::GET, "/nope"), Match::NotFound));
638    }
639
640    #[test]
641    fn known_path_wrong_method_is_405() {
642        let r = build();
643        match run(&r, Method::DELETE, "/users/7") {
644            Match::MethodNotAllowed { allow } => assert!(allow.contains(&Method::GET)),
645            _ => panic!("expected 405"),
646        }
647    }
648
649    #[test]
650    fn wildcard_captures_rest() {
651        let r = build();
652        match run(&r, Method::GET, "/files/a/b/c.txt") {
653            Match::Found { params, .. } => assert_eq!(params.get("path").unwrap(), "a/b/c.txt"),
654            _ => panic!("expected wildcard Found"),
655        }
656    }
657
658    // Regression guard for blocking issue #2: a pre-boxed `BoxHandler` must be
659    // acceptable to the route builder methods, not only closures.
660    #[tokio::test]
661    async fn route_builder_accepts_boxed_handler() {
662        let pre: BoxHandler = boxed((|_c: Call| async { "pre-boxed" }).into_handler());
663        let mut r = Router::new();
664        {
665            let mut b = RouteBuilder::new(&mut r);
666            b.get("/pre", pre);
667        }
668        match run(&r, Method::GET, "/pre") {
669            Match::Found { handler, .. } => {
670                let c = Call::new(
671                    Method::GET,
672                    "/pre".parse::<Uri>().unwrap(),
673                    HeaderMap::new(),
674                    Bytes::new(),
675                );
676                let res = handler.handle(c).await;
677                assert_eq!(res.body, Bytes::from("pre-boxed"));
678            }
679            _ => panic!("expected Found"),
680        }
681    }
682}