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/// Render an `Allow` header value from the methods explicitly registered for a
13/// path, adding the ones the dispatcher answers implicitly.
14///
15/// The single source of truth for `Allow`, whichever response carries it. RFC
16/// 9110 §15.5.6 (`405`) and §9.3.7 (`OPTIONS`) describe the same fact about the
17/// same resource, and generating it in two places is how they came to disagree:
18/// a path with one `GET` route told `OPTIONS` it supported `GET, HEAD, OPTIONS`
19/// and simultaneously told a `DELETE` that it supported only `GET`.
20///
21/// Returns `None` when nothing is registered — an empty `Allow` tells a client
22/// nothing, and the right answer there is `404` rather than `405`.
23///
24/// ```
25/// use churust_core::router::allow_header_value;
26/// use http::Method;
27///
28/// // A GET route also answers HEAD and OPTIONS.
29/// assert_eq!(allow_header_value(vec![Method::GET]).as_deref(), Some("GET, HEAD, OPTIONS"));
30/// // Without a GET there is no HEAD to synthesize.
31/// assert_eq!(allow_header_value(vec![Method::POST]).as_deref(), Some("OPTIONS, POST"));
32/// assert_eq!(allow_header_value(vec![]), None);
33/// ```
34pub fn allow_header_value(mut methods: Vec<Method>) -> Option<String> {
35 if methods.is_empty() {
36 return None;
37 }
38 // RFC 9110 §9.3.2: HEAD is available wherever GET is, and the dispatcher
39 // synthesizes it. Advertising it where there is no GET would be a promise
40 // the server cannot keep.
41 if methods.contains(&Method::GET) && !methods.contains(&Method::HEAD) {
42 methods.push(Method::HEAD);
43 }
44 // The dispatcher answers OPTIONS for any path that has any route.
45 if !methods.contains(&Method::OPTIONS) {
46 methods.push(Method::OPTIONS);
47 }
48 // Sorted so the header is deterministic: two responses describing one
49 // resource should be byte-identical, and tests can compare them directly.
50 methods.sort_by(|a, b| a.as_str().cmp(b.as_str()));
51 methods.dedup();
52 Some(
53 methods
54 .iter()
55 .map(Method::as_str)
56 .collect::<Vec<_>>()
57 .join(", "),
58 )
59}
60
61/// The outcome of routing a `(method, path)` pair against the [`Router`].
62///
63/// Returned by [`Router::route`]. The framework translates each variant into a
64/// response: [`Found`](Match::Found) runs the handler,
65/// [`MethodNotAllowed`](Match::MethodNotAllowed) yields `405` with an `Allow`
66/// header, and [`NotFound`](Match::NotFound) yields `404`.
67///
68/// ```
69/// use churust_core::{boxed, Call, IntoHandler, Router, Match};
70/// # use bytes::Bytes;
71/// # use http::HeaderMap;
72/// # fn probe(p: &str) -> Call {
73/// # Call::new(Method::GET, p.parse().unwrap(), HeaderMap::new(), Bytes::new())
74/// # }
75/// use http::Method;
76///
77/// let mut router = Router::new();
78/// router.add(Method::GET, "/users/{id}", boxed((|_c: Call| async { "ok" }).into_handler()));
79///
80/// match router.route(&Method::GET, "/users/7", &probe("/users/7")) {
81/// Match::Found { params, .. } => assert_eq!(params.get("id").unwrap(), "7"),
82/// _ => panic!("expected a match"),
83/// }
84/// assert!(matches!(router.route(&Method::GET, "/nope", &probe("/nope")), Match::NotFound));
85/// assert!(matches!(router.route(&Method::POST, "/users/7", &probe("/users/7")), Match::MethodNotAllowed { .. }));
86/// ```
87///
88/// Marked `#[non_exhaustive]`: a `match` on this enum outside `churust-core`
89/// must carry a `_` arm, so that a future routing outcome is not a breaking
90/// change.
91#[non_exhaustive]
92pub enum Match {
93 /// A handler matched the path and method. Carries the matched handler and
94 /// the captured path parameters (`{name}` -> value).
95 Found {
96 /// The handler registered for this `(path, method)`.
97 handler: BoxHandler,
98 /// The captured path parameters, keyed by name.
99 params: crate::call::Params,
100 },
101 /// The path matched a route, but not for this method. `allow` lists the
102 /// methods that *are* registered (used to build the `Allow` header).
103 MethodNotAllowed {
104 /// The methods registered for the matched path.
105 allow: Vec<Method>,
106 },
107 /// No route matched the path at all.
108 NotFound,
109 /// A path segment could not be percent-decoded — a malformed escape or
110 /// bytes that are not valid UTF-8. The dispatcher turns this into
111 /// `400 Bad Request`.
112 BadPath,
113}
114
115#[derive(Default)]
116struct Node {
117 statics: HashMap<String, Node>,
118 param: Option<(String, Box<Node>)>, // {name}
119 wildcard: Option<(String, BoxHandlers)>, // {name...} terminal
120 handlers: BoxHandlers,
121}
122
123/// One registered route: a handler plus the guards that must pass for it to
124/// serve. Several may share a method; the first whose guards pass wins.
125struct Candidate {
126 guards: Vec<crate::guard::BoxGuard>,
127 handler: BoxHandler,
128}
129
130#[derive(Default)]
131struct BoxHandlers(HashMap<Method, Vec<Candidate>>);
132
133impl BoxHandlers {
134 /// The first candidate whose guards all pass.
135 fn pick(&self, method: &Method, call: &crate::call::Call) -> Option<&BoxHandler> {
136 self.0
137 .get(method)?
138 .iter()
139 .find_map(|c| c.guards.iter().all(|g| g.check(call)).then_some(&c.handler))
140 }
141
142 /// Methods with at least one registered route, guards ignored. Used for
143 /// `Allow` on an `OPTIONS` request, which describes the resource rather
144 /// than any particular request.
145 fn methods(&self) -> Vec<Method> {
146 self.0.keys().cloned().collect()
147 }
148
149 /// Methods that could actually serve *this* request.
150 ///
151 /// A method whose every candidate fails its guards does not count: the
152 /// route does not match, so the answer is `404`, not `405`. Reporting it
153 /// as allowed would tell a client to retry a request that can never work.
154 fn methods_matching(&self, call: &crate::call::Call) -> Vec<Method> {
155 self.0
156 .iter()
157 .filter(|(_, cands)| cands.iter().any(|c| c.guards.iter().all(|g| g.check(call))))
158 .map(|(m, _)| m.clone())
159 .collect()
160 }
161
162 fn push(&mut self, method: Method, pattern: &str, handler: BoxHandler) {
163 let entries = self.0.entry(method.clone()).or_default();
164 // Only an unguarded duplicate is a mistake: guarded siblings are the
165 // whole point of guards.
166 if entries.iter().any(|c| c.guards.is_empty()) {
167 panic!("duplicate route: {method} {pattern} is already registered");
168 }
169 entries.push(Candidate {
170 guards: Vec::new(),
171 handler,
172 });
173 }
174}
175
176impl std::fmt::Debug for Node {
177 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178 f.debug_struct("Node").finish_non_exhaustive()
179 }
180}
181
182/// A compiled, trie-based router mapping `(method, path)` to a handler.
183///
184/// Build one with [`Router::new`], register routes with [`Router::add`], and
185/// look them up with [`Router::route`]. Inside an application you usually do not
186/// touch the `Router` directly — the
187/// [`RouteBuilder`] DSL in
188/// [`AppBuilder::routing`](crate::AppBuilder::routing) populates it for you.
189///
190/// Supported pattern syntax (full paths, leading `/`):
191/// - static segments: `/users/list`
192/// - named parameters: `/users/{id}` (captured as `id`)
193/// - trailing wildcard: `/files/{path...}` (captures the remaining path,
194/// slashes included; must be the last segment)
195///
196/// ```
197/// use churust_core::{boxed, Call, IntoHandler, Router, Match};
198/// # use bytes::Bytes;
199/// # use http::HeaderMap;
200/// # fn probe(p: &str) -> Call {
201/// # Call::new(Method::GET, p.parse().unwrap(), HeaderMap::new(), Bytes::new())
202/// # }
203/// use http::Method;
204///
205/// let mut router = Router::new();
206/// router.add(Method::GET, "/files/{path...}", boxed((|_c: Call| async { "" }).into_handler()));
207/// match router.route(&Method::GET, "/files/a/b/c.txt", &probe("/files/a/b/c.txt")) {
208/// Match::Found { params, .. } => assert_eq!(params.get("path").unwrap(), "a/b/c.txt"),
209/// _ => panic!("expected wildcard match"),
210/// }
211/// ```
212#[derive(Debug, Default)]
213pub struct Router {
214 root: Node,
215 /// Every `(method, pattern)` registered, in registration order.
216 ///
217 /// Kept alongside the trie rather than recovered from it: the trie stores
218 /// segments, so rebuilding a pattern would have to guess at how a parameter
219 /// was spelled, and anything reading this wants the spelling the
220 /// application actually wrote.
221 inventory: Vec<(Method, String)>,
222}
223
224impl Router {
225 /// Create an empty router. Equivalent to [`Router::default`].
226 pub fn new() -> Self {
227 Self::default()
228 }
229
230 /// Insert `handler` for `method` at `pattern` (the full path, e.g.
231 /// `/users/{id}`).
232 ///
233 /// Registering different methods at the same path is fine. Registering the
234 /// same `(method, path)` twice does *not* replace the earlier handler: an
235 /// unguarded duplicate is a mistake this router refuses to guess about, so
236 /// it panics. Several routes may share a `(method, path)` when guards tell
237 /// them apart — [`RouteBuilder::guard`] attaches one to the registration
238 /// just made — and those resolve first-match-wins in registration order, so
239 /// an unguarded route registered last is the fallback.
240 ///
241 /// # Panics
242 ///
243 /// Panics if a `{name...}` wildcard segment is not the final segment of the
244 /// pattern; if an unguarded route is already registered for this
245 /// `(method, path)`; or if a `{name}` at this position was already
246 /// registered under a different name, since one trie node holds one
247 /// parameter name and the second route's handler would look up a capture
248 /// that is not there.
249 ///
250 /// ```
251 /// use churust_core::{boxed, Call, IntoHandler, Router, Match};
252 /// # use bytes::Bytes;
253 /// # use http::HeaderMap;
254 /// # fn probe(p: &str) -> Call {
255 /// # Call::new(Method::GET, p.parse().unwrap(), HeaderMap::new(), Bytes::new())
256 /// # }
257 /// use http::Method;
258 ///
259 /// let mut router = Router::new();
260 /// router.add(Method::GET, "/ping", boxed((|_c: Call| async { "pong" }).into_handler()));
261 /// assert!(matches!(router.route(&Method::GET, "/ping", &probe("/ping")), Match::Found { .. }));
262 /// ```
263 pub fn add(&mut self, method: Method, pattern: &str, handler: BoxHandler) {
264 // Recorded before the trie insert, which may panic on a duplicate or a
265 // misplaced wildcard. A router that panicked is not going to be read.
266 self.inventory.push((method.clone(), pattern.to_string()));
267
268 let mut node = &mut self.root;
269 let segments: Vec<&str> = split_segments(pattern);
270 for (i, seg) in segments.iter().enumerate() {
271 if let Some(name) = seg.strip_prefix('{').and_then(|s| s.strip_suffix("...}")) {
272 // wildcard must be terminal
273 assert!(
274 i == segments.len() - 1,
275 "wildcard `{{{name}...}}` must be last segment"
276 );
277 let entry = node
278 .wildcard
279 .get_or_insert_with(|| (name.to_string(), BoxHandlers::default()));
280 entry.1.push(method, pattern, handler);
281 return;
282 } else if let Some(name) = seg.strip_prefix('{').and_then(|s| s.strip_suffix('}')) {
283 let entry = node
284 .param
285 .get_or_insert_with(|| (name.to_string(), Box::new(Node::default())));
286 // One node, one parameter name. Registering `/users/{id}` and
287 // then `/users/{name}/profile` used to silently reuse `id` for
288 // both, so the second route's handler looked up `name` and found
289 // nothing — a 400 at request time with no clue why. Failing here
290 // matches how this router already treats a misplaced wildcard
291 // and a duplicate route.
292 assert!(
293 entry.0 == name,
294 "conflicting path parameter names at the same position: \
295 `{{{}}}` and `{{{name}}}` (registering {method} {pattern})",
296 entry.0
297 );
298 node = entry.1.as_mut();
299 } else {
300 node = node.statics.entry(seg.to_string()).or_default();
301 }
302 }
303 node.handlers.push(method, pattern, handler);
304 }
305
306 /// Route `path` for `method`, returning a [`Match`].
307 ///
308 /// Matching prefers static segments over `{param}` captures, and falls back
309 /// to a `{name...}` wildcard at the deepest matchable ancestor. A path that
310 /// matches a node with no handler for `method` yields
311 /// [`Match::MethodNotAllowed`]; a path that matches no node yields
312 /// [`Match::NotFound`].
313 ///
314 /// ```
315 /// use churust_core::{boxed, Call, IntoHandler, Router, Match};
316 /// # use bytes::Bytes;
317 /// # use http::HeaderMap;
318 /// # fn probe(p: &str) -> Call {
319 /// # Call::new(Method::GET, p.parse().unwrap(), HeaderMap::new(), Bytes::new())
320 /// # }
321 /// use http::Method;
322 ///
323 /// let mut router = Router::new();
324 /// router.add(Method::GET, "/", boxed((|_c: Call| async { "home" }).into_handler()));
325 /// assert!(matches!(router.route(&Method::GET, "/", &probe("/")), Match::Found { .. }));
326 /// assert!(matches!(router.route(&Method::GET, "/missing", &probe("/missing")), Match::NotFound));
327 /// ```
328 pub fn route(&self, method: &Method, path: &str, call: &crate::call::Call) -> Match {
329 // Split first, decode second. Decoding before splitting would let %2F
330 // manufacture separators and forge extra path segments.
331 let decoded = match decode_segments(path) {
332 Some(d) => d,
333 None => return Match::BadPath,
334 };
335 let segments: Vec<&str> = decoded.iter().map(String::as_str).collect();
336 let mut params = crate::call::Params::new();
337
338 // 1. Exact search. Static beats param, and a leaf that cannot serve
339 // this request is passed over rather than settled for.
340 let found = Self::walk_matching(&self.root, &segments, 0, &mut params, &mut |node, p| {
341 node.handlers
342 .pick(method, call)
343 .map(|h| (h.clone(), p.clone()))
344 });
345 if let Some((handler, params)) = found {
346 return Match::Found { handler, params };
347 }
348
349 // Nothing served it. `Allow` then has to describe every leaf that
350 // matched structurally, not just the first — they are all the same
351 // resource as far as the client is concerned.
352 let mut exact_allow: Vec<Method> = Vec::new();
353 params.clear();
354 Self::walk_matching(&self.root, &segments, 0, &mut params, &mut |node,
355 _|
356 -> Option<
357 (),
358 > {
359 for m in node.handlers.methods_matching(call) {
360 if !exact_allow.contains(&m) {
361 exact_allow.push(m);
362 }
363 }
364 // Never accept: visiting every matching leaf is the point.
365 None
366 });
367
368 // 2. Wildcard fallback. Reaching a node that has no handler for this
369 // method is not the end of the search — a trailing `{name...}` at a
370 // shallower depth may still serve the request. Without this, any
371 // static route sharing a wildcard's prefix hides it entirely.
372 //
373 // The walk above may have written captures into `params`. They
374 // belong to the branch just abandoned and must not reach the
375 // wildcard handler.
376 params.clear();
377 match Self::walk_wildcard(&self.root, &segments, 0, method, call, &mut params) {
378 Some(found @ Match::Found { .. }) => found,
379 Some(Match::MethodNotAllowed { allow: wild_allow }) => {
380 let mut allow = exact_allow;
381 for m in wild_allow {
382 if !allow.contains(&m) {
383 allow.push(m);
384 }
385 }
386 Match::MethodNotAllowed { allow }
387 }
388 _ if !exact_allow.is_empty() => Match::MethodNotAllowed { allow: exact_allow },
389 _ => Match::NotFound,
390 }
391 }
392
393 /// Attach a guard to the most recently registered route at
394 /// `(method, pattern)`.
395 ///
396 /// Used by [`RouteBuilder::guard`]; registration order is what makes "most
397 /// recent" unambiguous.
398 fn attach_guard(&mut self, method: &Method, pattern: &str, g: crate::guard::BoxGuard) {
399 let mut node = &mut self.root;
400 let segments: Vec<&str> = split_segments(pattern);
401 for (i, seg) in segments.iter().enumerate() {
402 if let Some(_name) = seg.strip_prefix('{').and_then(|s| s.strip_suffix("...}")) {
403 debug_assert!(i == segments.len() - 1);
404 if let Some(entry) = node.wildcard.as_mut() {
405 if let Some(c) = entry.1 .0.get_mut(method).and_then(|v| v.last_mut()) {
406 c.guards.push(g);
407 }
408 }
409 return;
410 } else if seg.starts_with('{') && seg.ends_with('}') {
411 match node.param.as_mut() {
412 Some(entry) => node = entry.1.as_mut(),
413 None => return,
414 }
415 } else {
416 match node.statics.get_mut(*seg) {
417 Some(child) => node = child,
418 None => return,
419 }
420 }
421 }
422 if let Some(c) = node.handlers.0.get_mut(method).and_then(|v| v.last_mut()) {
423 c.guards.push(g);
424 }
425 }
426
427 /// Wrap the most recently registered route at `(method, pattern)` so it
428 /// carries a per-route body limit.
429 fn attach_limit(&mut self, method: &Method, pattern: &str, max_body_bytes: usize) {
430 let mut node = &mut self.root;
431 let segments: Vec<&str> = split_segments(pattern);
432 for (i, seg) in segments.iter().enumerate() {
433 if seg
434 .strip_prefix('{')
435 .and_then(|s| s.strip_suffix("...}"))
436 .is_some()
437 {
438 debug_assert!(i == segments.len() - 1);
439 if let Some(entry) = node.wildcard.as_mut() {
440 if let Some(c) = entry.1 .0.get_mut(method).and_then(|v| v.last_mut()) {
441 let inner = c.handler.clone();
442 c.handler = std::sync::Arc::new(LimitedHandler {
443 max_body_bytes,
444 inner,
445 });
446 }
447 }
448 return;
449 } else if seg.starts_with('{') && seg.ends_with('}') {
450 match node.param.as_mut() {
451 Some(entry) => node = entry.1.as_mut(),
452 None => return,
453 }
454 } else {
455 match node.statics.get_mut(*seg) {
456 Some(child) => node = child,
457 None => return,
458 }
459 }
460 }
461 if let Some(c) = node.handlers.0.get_mut(method).and_then(|v| v.last_mut()) {
462 let inner = c.handler.clone();
463 c.handler = std::sync::Arc::new(LimitedHandler {
464 max_body_bytes,
465 inner,
466 });
467 }
468 }
469
470 /// The methods registered for `path`, including any a trailing wildcard
471 /// would serve. Empty when the path matches no route at all.
472 ///
473 /// Used by the dispatcher to build the `Allow` header for an `OPTIONS`
474 /// request that has no handler of its own.
475 ///
476 /// ```
477 /// use churust_core::{boxed, Call, IntoHandler, Router};
478 /// use http::Method;
479 ///
480 /// let mut router = Router::new();
481 /// router.add(Method::GET, "/x", boxed((|_c: Call| async { "" }).into_handler()));
482 /// assert_eq!(router.methods_for("/x"), vec![Method::GET]);
483 /// assert!(router.methods_for("/nope").is_empty());
484 /// ```
485 pub fn methods_for(&self, path: &str) -> Vec<Method> {
486 let decoded = match decode_segments(path) {
487 Some(d) => d,
488 None => return Vec::new(),
489 };
490 let segments: Vec<&str> = decoded.iter().map(String::as_str).collect();
491 let mut params = crate::call::Params::new();
492 // Every structurally-matching leaf, not just the first: `OPTIONS`
493 // describes the resource, and two routes of different shapes matching
494 // one path are one resource to the client.
495 let mut out: Vec<Method> = Vec::new();
496 Self::walk_matching(&self.root, &segments, 0, &mut params, &mut |node,
497 _|
498 -> Option<
499 (),
500 > {
501 for m in node.handlers.methods() {
502 if !out.contains(&m) {
503 out.push(m);
504 }
505 }
506 None
507 });
508
509 // The wildcard branch is enumerated directly, the same guard-free way
510 // the exact branch above is. It used to be driven by `walk_wildcard`
511 // with a synthetic `TRACE` call standing in for a real one, on the
512 // reasoning that nothing registers `TRACE`, so the walk would report
513 // `MethodNotAllowed` carrying the wildcard's method list. Both halves of
514 // that were wrong. `walk_wildcard` resolves guards against the call it
515 // is given, and a synthetic call has no headers and no authority, so it
516 // fails every guard there is: one `guard::host` on a `{p...}` route left
517 // `methods_for` empty, the dispatcher skipped its `204` arm, and the
518 // request fell through to the `405` path — where the real call *does*
519 // pass the guard, so the reply advertised `Allow: GET, HEAD, OPTIONS`
520 // while refusing the `OPTIONS` it had just named. It also assumed no
521 // application registers `TRACE`; one that does turned the probe into a
522 // `Found`, which the `if let` discarded, losing the list entirely.
523 Self::collect_wildcard_methods(&self.root, &segments, 0, &mut out);
524 out
525 }
526
527 /// Union the methods of every wildcard a path could reach, guards ignored.
528 ///
529 /// Mirrors `walk_wildcard`'s structural descent — statics and params first,
530 /// this node's own wildcard last — but unions instead of choosing, because
531 /// `Allow` describes a resource rather than the fate of one request:
532 /// `walk_wildcard` stops at the first wildcard that can serve, while every
533 /// wildcard reachable along the path is part of what this URL supports, and
534 /// that is exactly what `walk_wildcard` already unions into `Allow` when
535 /// none of them can serve.
536 fn collect_wildcard_methods(node: &Node, segs: &[&str], i: usize, out: &mut Vec<Method>) {
537 if i < segs.len() {
538 if let Some(child) = node.statics.get(segs[i]) {
539 Self::collect_wildcard_methods(child, segs, i + 1, out);
540 }
541 if let Some((_, child)) = &node.param {
542 Self::collect_wildcard_methods(child, segs, i + 1, out);
543 }
544 }
545 if let Some((_, handlers)) = &node.wildcard {
546 // The same refusal `walk_wildcard` applies: a tail whose decoded
547 // segments contain a separator is not a request this wildcard will
548 // ever serve, so advertising its methods would promise a `200` that
549 // routing has already decided against.
550 if segs[i..]
551 .iter()
552 .any(|s| s.contains('/') || s.contains('\\'))
553 {
554 return;
555 }
556 for m in handlers.methods() {
557 if !out.contains(&m) {
558 out.push(m);
559 }
560 }
561 }
562 }
563
564 /// Every method registered anywhere in this router, deduplicated.
565 ///
566 /// Answers `OPTIONS *`, which RFC 9110 §9.3.7 defines as a question about
567 /// the server rather than about any one resource.
568 ///
569 /// ```
570 /// use churust_core::{boxed, Call, IntoHandler, Router};
571 /// use http::Method;
572 ///
573 /// let mut router = Router::new();
574 /// router.add(Method::GET, "/a", boxed((|_c: Call| async { "" }).into_handler()));
575 /// router.add(Method::POST, "/b", boxed((|_c: Call| async { "" }).into_handler()));
576 /// let mut all = router.all_methods();
577 /// all.sort_by(|a, b| a.as_str().cmp(b.as_str()));
578 /// assert_eq!(all, vec![Method::GET, Method::POST]);
579 /// ```
580 pub fn all_methods(&self) -> Vec<Method> {
581 let mut out = Vec::new();
582 Self::collect_methods(&self.root, &mut out);
583 out
584 }
585
586 /// Every registered route as `(method, pattern)`, in registration order.
587 ///
588 /// The patterns are exactly what was registered, `{id}` and `{path...}`
589 /// included, which is what makes this usable as the inventory an API
590 /// description is generated from. Routes that share a `(method, path)`
591 /// behind different guards appear once per registration, because each is a
592 /// separate route even though a client sees one URL.
593 ///
594 /// ```
595 /// use churust_core::{boxed, Call, IntoHandler, Router};
596 /// use http::Method;
597 ///
598 /// let mut router = Router::new();
599 /// router.add(Method::GET, "/users/{id}", boxed((|_c: Call| async { "" }).into_handler()));
600 /// assert_eq!(router.routes(), &[(Method::GET, "/users/{id}".to_string())]);
601 /// ```
602 pub fn routes(&self) -> &[(Method, String)] {
603 &self.inventory
604 }
605
606 fn collect_methods(node: &Node, out: &mut Vec<Method>) {
607 for m in node.handlers.methods() {
608 if !out.contains(&m) {
609 out.push(m);
610 }
611 }
612 if let Some((_, handlers)) = &node.wildcard {
613 for m in handlers.methods() {
614 if !out.contains(&m) {
615 out.push(m);
616 }
617 }
618 }
619 for child in node.statics.values() {
620 Self::collect_methods(child, out);
621 }
622 if let Some((_, child)) = &node.param {
623 Self::collect_methods(child, out);
624 }
625 }
626
627 /// Visit every leaf whose *shape* matches, in precedence order, and return
628 /// the first result `accept` produces.
629 ///
630 /// The visitor is what makes this a search rather than a lookup. Returning
631 /// the first structurally-matching leaf regardless of whether it could
632 /// serve meant a second registered route that *did* match was unreachable:
633 /// with `POST /a/{x}` and `GET /{y}/b` registered, `GET /a/b` committed to
634 /// the first and answered `405`. A failing guard had the same effect, so a
635 /// guard on one route silently disabled an unrelated one.
636 ///
637 /// Static still beats param at each step — that is a preference, and it
638 /// survives; it just no longer ends the search. `params` is restored on the
639 /// way out of a branch that is abandoned, so captures never leak from a
640 /// route that did not serve into one that did.
641 fn walk_matching<'a, T>(
642 node: &'a Node,
643 segs: &[&str],
644 i: usize,
645 params: &mut crate::call::Params,
646 accept: &mut impl FnMut(&'a Node, &crate::call::Params) -> Option<T>,
647 ) -> Option<T> {
648 if i == segs.len() {
649 return accept(node, params);
650 }
651 let seg = segs[i];
652 if let Some(child) = node.statics.get(seg) {
653 if let Some(found) = Self::walk_matching(child, segs, i + 1, params, accept) {
654 return Some(found);
655 }
656 }
657 if let Some((name, child)) = &node.param {
658 // Remember any value this name already held: a repeated name at
659 // different depths must not be clobbered by an abandoned branch.
660 let previous = params.get(name).map(str::to_string);
661 params.insert(name.clone(), seg.to_string());
662 if let Some(found) = Self::walk_matching(child, segs, i + 1, params, accept) {
663 return Some(found);
664 }
665 match previous {
666 Some(v) => params.insert(name.clone(), v),
667 None => params.remove(name),
668 }
669 }
670 None
671 }
672
673 #[allow(clippy::too_many_arguments)]
674 fn walk_wildcard(
675 node: &Node,
676 segs: &[&str],
677 i: usize,
678 method: &Method,
679 call: &crate::call::Call,
680 params: &mut crate::call::Params,
681 ) -> Option<Match> {
682 // Descend before considering this node's own wildcard, so the *deepest*
683 // wildcard wins. Consulting it first made a shallow `{p...}` shadow every
684 // deeper one: with `/files/{p...}` and `/files/img/{q...}` registered,
685 // nothing under `/files/img/` could ever reach the second handler, and
686 // nothing reported the shadowing at registration or at match time.
687 //
688 // A `MethodNotAllowed` found deeper is remembered rather than returned:
689 // a shallower wildcard may still have a real handler for this method,
690 // and a `405` must not pre-empt a `200`.
691 let mut method_mismatch: Option<Match> = None;
692 if i < segs.len() {
693 if let Some(child) = node.statics.get(segs[i]) {
694 match Self::walk_wildcard(child, segs, i + 1, method, call, params) {
695 Some(found @ Match::Found { .. }) => return Some(found),
696 Some(other) => method_mismatch = Some(other),
697 None => {}
698 }
699 }
700 if let Some((pname, child)) = &node.param {
701 params.insert(pname.clone(), segs[i].to_string());
702 match Self::walk_wildcard(child, segs, i + 1, method, call, params) {
703 Some(found @ Match::Found { .. }) => return Some(found),
704 Some(other) => {
705 // The capture belongs to a branch we are abandoning.
706 params.remove(pname);
707 method_mismatch = Some(other);
708 }
709 None => params.remove(pname),
710 }
711 }
712 }
713
714 if let Some((name, handlers)) = &node.wildcard {
715 // Refuse a tail whose segments decoded to something containing a
716 // separator. Segments are split before decoding precisely so `%2F`
717 // cannot manufacture one — rejoining the decoded pieces with `/`
718 // handed that back, making `/files/a%2Fb/c` indistinguishable from
719 // `/files/a/b/c` for every consumer of the capture. `StaticFiles`
720 // has its own guard; nothing else did.
721 //
722 // This `BadPath` refuses the wildcard, not the request. `route`
723 // deliberately does not forward it as the `400` the variant is
724 // named for: the tail being unusable *here* says nothing about the
725 // rest of the trie, and a sibling `{n}` may match the same path
726 // cleanly with the separator inside one capture — which is the
727 // whole point of splitting before decoding. Promoting this into a
728 // `400` would overrule that sibling and report a malformed request
729 // where there is none. The wildcard simply stops matching, so the
730 // request lands on whatever else does, or on `404`.
731 if segs[i..]
732 .iter()
733 .any(|s| s.contains('/') || s.contains('\\'))
734 {
735 return Some(Match::BadPath);
736 }
737 let rest = segs[i..].join("/");
738 params.insert(name.clone(), rest);
739 return Some(match handlers.pick(method, call) {
740 Some(h) => Match::Found {
741 handler: h.clone(),
742 params: std::mem::take(params),
743 },
744 None => {
745 params.remove(name);
746 let mut allow = handlers.methods_matching(call);
747 // Both depths reject the method, so `Allow` must describe
748 // every method either of them would have served.
749 if let Some(Match::MethodNotAllowed { allow: deeper }) = method_mismatch {
750 for m in deeper {
751 if !allow.contains(&m) {
752 allow.push(m);
753 }
754 }
755 }
756 Match::MethodNotAllowed { allow }
757 }
758 });
759 }
760 method_mismatch
761 }
762}
763
764/// Seeds a per-route body limit into the call before delegating.
765///
766/// The engine's global cap still bounds what is read off the socket; this is
767/// the per-route tightening that extractors consult, which is the same place
768/// per-route extractor configuration belongs.
769struct LimitedHandler {
770 max_body_bytes: usize,
771 inner: BoxHandler,
772}
773
774impl crate::handler::Handler for LimitedHandler {
775 fn handle(&self, mut call: crate::call::Call) -> crate::handler::HandlerFuture {
776 call.insert(crate::extract::RouteBodyLimit(self.max_body_bytes));
777 let inner = self.inner.clone();
778 Box::pin(async move { inner.handle(call).await })
779 }
780}
781
782/// A handler with a scope's middleware chain wrapped around it.
783///
784/// Built once at route registration rather than consulted per request, so a
785/// route in no scope is exactly as cheap as before.
786struct ScopedHandler {
787 chain: Vec<std::sync::Arc<dyn crate::pipeline::Middleware>>,
788 inner: BoxHandler,
789}
790
791impl crate::handler::Handler for ScopedHandler {
792 fn handle(&self, call: crate::call::Call) -> crate::handler::HandlerFuture {
793 let inner = self.inner.clone();
794 let endpoint: crate::pipeline::Endpoint = std::sync::Arc::new(move |call| {
795 let inner = inner.clone();
796 Box::pin(async move { inner.handle(call).await })
797 });
798 let chain: std::collections::VecDeque<_> = self.chain.iter().cloned().collect();
799 Box::pin(crate::pipeline::Next::new(chain, endpoint).run(call))
800 }
801}
802
803fn split_segments(path: &str) -> Vec<&str> {
804 path.split('/').filter(|s| !s.is_empty()).collect()
805}
806
807/// Split `path` on `/` and percent-decode each segment individually.
808///
809/// The order is the point. Decoding the whole path first would turn `%2F` into
810/// a separator and let a request forge segments it was never routed through.
811/// Returns `None` if any segment is undecodable, which becomes `400`.
812fn decode_segments(path: &str) -> Option<Vec<String>> {
813 split_segments(path)
814 .into_iter()
815 .map(crate::path::decode_path_segment)
816 .collect()
817}
818
819/// The route-definition DSL handed to the closure in
820/// [`AppBuilder::routing`](crate::AppBuilder::routing).
821///
822/// Register handlers with the per-method helpers ([`get`](RouteBuilder::get),
823/// [`post`](RouteBuilder::post), [`put`](RouteBuilder::put),
824/// [`delete`](RouteBuilder::delete)) or the generic [`method`](RouteBuilder::method).
825/// Group related routes under a common prefix with [`route`](RouteBuilder::route),
826/// which nests cleanly. Each handler may be an extractor closure or anything
827/// implementing [`Handler`](crate::Handler).
828///
829/// ```
830/// use churust_core::{Churust, Call, TestClient};
831/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
832/// let app = Churust::server()
833/// .routing(|r| {
834/// r.get("/", |_c: Call| async { "home" });
835/// r.route("/api", |r| {
836/// r.get("/health", |_c: Call| async { "ok" });
837/// });
838/// })
839/// .build();
840/// let res = TestClient::new(app).get("/api/health").send().await;
841/// assert_eq!(res.text(), "ok");
842/// # });
843/// ```
844pub struct RouteBuilder<'r> {
845 router: &'r mut Router,
846 prefix: String,
847 /// Middleware applied to routes registered in this scope, outermost first.
848 /// Nested scopes inherit a clone, so a child cannot affect its parent.
849 chain: Vec<std::sync::Arc<dyn crate::pipeline::Middleware>>,
850 /// The most recent `(method, full path)` registered here, so `guard` knows
851 /// which route it modifies.
852 last: Option<(Method, String)>,
853}
854
855impl<'r> RouteBuilder<'r> {
856 pub(crate) fn new(router: &'r mut Router) -> Self {
857 Self {
858 router,
859 prefix: String::new(),
860 chain: Vec::new(),
861 last: None,
862 }
863 }
864
865 /// Apply `mw` to every route registered in this scope **after** this call,
866 /// including those in nested scopes.
867 ///
868 /// This is v1 design §6's scope-local interception, and the equivalent of
869 /// scope-local interception. Ordering is deliberate: a route registered before
870 /// the `intercept` call is not covered by it, so the reading order of the
871 /// block matches the behaviour.
872 ///
873 /// ```
874 /// use churust_core::{Call, Churust, Middleware, Next, Response, TestClient};
875 /// use async_trait::async_trait;
876 /// use http::StatusCode;
877 ///
878 /// struct RequireKey;
879 /// #[async_trait]
880 /// impl Middleware for RequireKey {
881 /// async fn handle(&self, call: Call, next: Next) -> Response {
882 /// if call.header("x-key").is_some() {
883 /// next.run(call).await
884 /// } else {
885 /// Response::new(StatusCode::UNAUTHORIZED)
886 /// }
887 /// }
888 /// }
889 ///
890 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
891 /// let app = Churust::server()
892 /// .routing(|r| {
893 /// r.get("/open", |_c: Call| async { "open" });
894 /// r.route("/admin", |r| {
895 /// r.intercept(RequireKey);
896 /// r.get("/panel", |_c: Call| async { "panel" });
897 /// });
898 /// })
899 /// .build();
900 ///
901 /// let client = TestClient::new(app);
902 /// assert_eq!(client.get("/open").send().await.status(), StatusCode::OK);
903 /// assert_eq!(
904 /// client.get("/admin/panel").send().await.status(),
905 /// StatusCode::UNAUTHORIZED
906 /// );
907 /// # });
908 /// ```
909 pub fn intercept<M: crate::pipeline::Middleware>(&mut self, mw: M) -> &mut Self {
910 self.chain.push(std::sync::Arc::new(mw));
911 self
912 }
913
914 fn full(&self, path: &str) -> String {
915 let mut p = self.prefix.clone();
916 if !path.starts_with('/') {
917 p.push('/');
918 }
919 p.push_str(path);
920 p
921 }
922
923 /// Register a handler for `method` at `path`. Accepts both extractor
924 /// closures (via the `HandlerFn` family) and anything already implementing
925 /// `Handler` — including a pre-boxed `BoxHandler` — through `IntoHandler`.
926 pub fn method<Marker, H>(&mut self, method: Method, path: &str, handler: H) -> &mut Self
927 where
928 H: IntoHandler<Marker>,
929 {
930 let full = self.full(path);
931 let h = boxed(handler.into_handler());
932 // Wrap once, at registration: the scope chain is fixed by then, so the
933 // request path pays nothing for scopes it is not in.
934 let h = if self.chain.is_empty() {
935 h
936 } else {
937 std::sync::Arc::new(ScopedHandler {
938 chain: self.chain.clone(),
939 inner: h,
940 }) as BoxHandler
941 };
942 self.router.add(method.clone(), &full, h);
943 self.last = Some((method, full));
944 self
945 }
946
947 /// Attach a [`Guard`](crate::Guard) to the route just registered.
948 ///
949 /// Routes may share a `(method, path)` when guards distinguish them; the
950 /// first whose guards all pass serves the request, in registration order,
951 /// so an unguarded route registered last is the fallback. A request that
952 /// matches no candidate is a `404`.
953 ///
954 /// ```
955 /// use churust_core::{guard, Call, Churust, TestClient};
956 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
957 /// let app = Churust::server()
958 /// .routing(|r| {
959 /// r.get("/", |_c: Call| async { "beta" })
960 /// .guard(guard::header("x-beta", "1"));
961 /// r.get("/", |_c: Call| async { "stable" });
962 /// })
963 /// .build();
964 /// let client = TestClient::new(app);
965 /// assert_eq!(client.get("/").header("x-beta", "1").send().await.text(), "beta");
966 /// assert_eq!(client.get("/").send().await.text(), "stable");
967 /// # });
968 /// ```
969 ///
970 /// # Panics
971 ///
972 /// Panics if no route has been registered on this builder yet — there would
973 /// be nothing for the guard to apply to.
974 pub fn guard(&mut self, g: crate::guard::BoxGuard) -> &mut Self {
975 let (method, path) = self
976 .last
977 .clone()
978 .expect("guard() must follow a route registration");
979 self.router.attach_guard(&method, &path, g);
980 self
981 }
982
983 /// Cap the request body for the route just registered.
984 ///
985 /// The server-wide [`max_body_bytes`](crate::AppBuilder::max_body_bytes)
986 /// still bounds what is read off the socket; this tightens it for one
987 /// route, which is what lets an upload endpoint be generous while the rest
988 /// of the API stays strict. Body extractors (`Json<T>`, `Form<T>`) enforce
989 /// it and answer `413`.
990 ///
991 /// ```
992 /// use churust_core::{Call, Churust, Form, TestClient};
993 /// use http::StatusCode;
994 /// use serde::Deserialize;
995 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
996 /// #[derive(Deserialize)]
997 /// struct Note { text: String }
998 ///
999 /// let app = Churust::server()
1000 /// .routing(|r| {
1001 /// r.post("/tiny", |Form(n): Form<Note>| async move { n.text })
1002 /// .max_body_bytes(8);
1003 /// })
1004 /// .build();
1005 ///
1006 /// let res = TestClient::new(app)
1007 /// .post("/tiny")
1008 /// .header("content-type", "application/x-www-form-urlencoded")
1009 /// .body("text=this-is-far-too-long")
1010 /// .send()
1011 /// .await;
1012 /// assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE);
1013 /// # });
1014 /// ```
1015 ///
1016 /// # Panics
1017 ///
1018 /// Panics if no route has been registered on this builder yet.
1019 pub fn max_body_bytes(&mut self, n: usize) -> &mut Self {
1020 let (method, path) = self
1021 .last
1022 .clone()
1023 .expect("max_body_bytes() must follow a route registration");
1024 self.router.attach_limit(&method, &path, n);
1025 self
1026 }
1027
1028 /// Register a `GET` handler at `path`. Returns `&mut Self` for chaining.
1029 pub fn get<Marker, H>(&mut self, path: &str, handler: H) -> &mut Self
1030 where
1031 H: IntoHandler<Marker>,
1032 {
1033 self.method(Method::GET, path, handler)
1034 }
1035 /// Register a `POST` handler at `path`. Returns `&mut Self` for chaining.
1036 pub fn post<Marker, H>(&mut self, path: &str, handler: H) -> &mut Self
1037 where
1038 H: IntoHandler<Marker>,
1039 {
1040 self.method(Method::POST, path, handler)
1041 }
1042 /// Register a `PUT` handler at `path`. Returns `&mut Self` for chaining.
1043 pub fn put<Marker, H>(&mut self, path: &str, handler: H) -> &mut Self
1044 where
1045 H: IntoHandler<Marker>,
1046 {
1047 self.method(Method::PUT, path, handler)
1048 }
1049 /// Register a `DELETE` handler at `path`. Returns `&mut Self` for chaining.
1050 pub fn delete<Marker, H>(&mut self, path: &str, handler: H) -> &mut Self
1051 where
1052 H: IntoHandler<Marker>,
1053 {
1054 self.method(Method::DELETE, path, handler)
1055 }
1056
1057 /// Open a nested scope: every route registered inside `f` has `path`
1058 /// prepended to its pattern. Scopes nest arbitrarily, so prefixes compose.
1059 /// Returns `&mut Self` for chaining.
1060 pub fn route(&mut self, path: &str, f: impl FnOnce(&mut RouteBuilder)) -> &mut Self {
1061 let prefix = self.full(path);
1062 let mut child = RouteBuilder {
1063 router: self.router,
1064 prefix,
1065 chain: self.chain.clone(),
1066 last: None,
1067 };
1068 f(&mut child);
1069 // Forget which route was registered *before* the scope. `guard()` and
1070 // `max_body_bytes()` resolve against `last`, so leaving it set meant a
1071 // `.guard(...)` chained after `route(...)` silently attached to the
1072 // previous sibling — arming the wrong route and leaving the scoped one
1073 // open. With `last` cleared, that chain panics at build instead.
1074 self.last = None;
1075 self
1076 }
1077}
1078
1079#[cfg(test)]
1080mod tests {
1081 use super::*;
1082 use crate::call::Call;
1083 use bytes::Bytes;
1084 use http::{HeaderMap, StatusCode, Uri};
1085
1086 fn build() -> Router {
1087 let mut r = Router::new();
1088 {
1089 let mut b = RouteBuilder::new(&mut r);
1090 b.get("/", |_c: Call| async { "root" });
1091 b.route("/users", |b| {
1092 b.get("/{id}", |c: Call| async move {
1093 format!("user {}", c.param_raw("id").unwrap())
1094 });
1095 b.post("/", |_c: Call| async { (StatusCode::CREATED, "created") });
1096 });
1097 b.get("/files/{path...}", |c: Call| async move {
1098 format!("file {}", c.param_raw("path").unwrap())
1099 });
1100 }
1101 r
1102 }
1103
1104 /// A bare call for tests that exercise routing rather than guards.
1105 fn probe(path: &str) -> Call {
1106 Call::new(
1107 Method::GET,
1108 path.parse::<Uri>().unwrap_or_else(|_| "/".parse().unwrap()),
1109 HeaderMap::new(),
1110 Bytes::new(),
1111 )
1112 }
1113
1114 fn run(r: &Router, m: Method, path: &str) -> Match {
1115 r.route(&m, path, &probe(path))
1116 }
1117
1118 #[tokio::test]
1119 async fn matches_static_and_param() {
1120 let r = build();
1121 match run(&r, Method::GET, "/users/7") {
1122 Match::Found { handler, params } => {
1123 assert_eq!(params.get("id").unwrap(), "7");
1124 let mut c = Call::new(
1125 Method::GET,
1126 "/users/7".parse::<Uri>().unwrap(),
1127 HeaderMap::new(),
1128 Bytes::new(),
1129 );
1130 c.set_params(params);
1131 let res = handler.handle(c).await;
1132 assert_eq!(res.body, Bytes::from("user 7"));
1133 }
1134 _ => panic!("expected Found"),
1135 }
1136 }
1137
1138 fn build_shadowed() -> Router {
1139 let mut r = Router::new();
1140 {
1141 let mut b = RouteBuilder::new(&mut r);
1142 b.get("/files/{path...}", |c: Call| async move {
1143 format!("wild:{}", c.param_raw("path").unwrap_or(""))
1144 });
1145 b.get("/files/special/x", |_c: Call| async { "static" });
1146 b.post("/files/only-post", |_c: Call| async { "posted" });
1147 }
1148 r
1149 }
1150
1151 #[test]
1152 fn path_params_are_percent_decoded() {
1153 let mut r = Router::new();
1154 {
1155 let mut b = RouteBuilder::new(&mut r);
1156 b.get("/u/{name}", |_c: Call| async { "" });
1157 }
1158 match r.route(&Method::GET, "/u/John%20Doe", &probe("/u/John%20Doe")) {
1159 Match::Found { params, .. } => assert_eq!(params.get("name").unwrap(), "John Doe"),
1160 _ => panic!("expected a match"),
1161 }
1162 }
1163
1164 #[test]
1165 fn encoded_slash_does_not_create_a_segment() {
1166 let mut r = Router::new();
1167 {
1168 let mut b = RouteBuilder::new(&mut r);
1169 b.get("/u/{name}", |_c: Call| async { "" });
1170 }
1171 // Matching at all proves %2F stayed inside one segment. Had it been
1172 // decoded before splitting, this would be two segments and miss.
1173 match r.route(&Method::GET, "/u/a%2Fb", &probe("/u/a%2Fb")) {
1174 Match::Found { params, .. } => assert_eq!(params.get("name").unwrap(), "a/b"),
1175 _ => panic!("%2F must not manufacture a separator"),
1176 }
1177 }
1178
1179 #[test]
1180 fn a_route_with_a_literal_space_is_reachable_encoded() {
1181 let mut r = Router::new();
1182 {
1183 let mut b = RouteBuilder::new(&mut r);
1184 b.get("/a b", |_c: Call| async { "" });
1185 }
1186 assert!(matches!(
1187 r.route(&Method::GET, "/a%20b", &probe("/a%20b")),
1188 Match::Found { .. }
1189 ));
1190 }
1191
1192 #[test]
1193 fn malformed_encoding_is_a_bad_path() {
1194 let mut r = Router::new();
1195 {
1196 let mut b = RouteBuilder::new(&mut r);
1197 b.get("/u/{name}", |_c: Call| async { "" });
1198 }
1199 assert!(matches!(
1200 r.route(&Method::GET, "/u/%zz", &probe("/u/%zz")),
1201 Match::BadPath
1202 ));
1203 assert!(matches!(
1204 r.route(&Method::GET, "/u/%FF", &probe("/u/%FF")),
1205 Match::BadPath
1206 ));
1207 }
1208
1209 #[test]
1210 fn wildcard_captures_decoded_segments() {
1211 let mut r = Router::new();
1212 {
1213 let mut b = RouteBuilder::new(&mut r);
1214 b.get("/f/{rest...}", |_c: Call| async { "" });
1215 }
1216 match r.route(&Method::GET, "/f/a%20b/c", &probe("/f/a%20b/c")) {
1217 Match::Found { params, .. } => assert_eq!(params.get("rest").unwrap(), "a b/c"),
1218 _ => panic!("expected the wildcard to match"),
1219 }
1220 }
1221
1222 #[test]
1223 fn wildcard_is_reachable_through_a_static_sibling() {
1224 let r = build_shadowed();
1225 match run(&r, Method::GET, "/files/special") {
1226 Match::Found { params, .. } => {
1227 assert_eq!(params.get("path").unwrap(), "special");
1228 }
1229 _ => panic!("wildcard should serve /files/special"),
1230 }
1231 }
1232
1233 #[test]
1234 fn exact_match_still_wins_over_wildcard() {
1235 let r = build_shadowed();
1236 match run(&r, Method::GET, "/files/special/x") {
1237 Match::Found { params, .. } => {
1238 assert!(
1239 !params.contains_key("path"),
1240 "the static route captured a wildcard param"
1241 );
1242 }
1243 _ => panic!("expected the static route"),
1244 }
1245 }
1246
1247 #[test]
1248 fn allow_header_unions_exact_and_wildcard_methods() {
1249 let r = build_shadowed();
1250 match run(&r, Method::DELETE, "/files/only-post") {
1251 Match::MethodNotAllowed { allow } => {
1252 assert!(allow.contains(&Method::POST), "missing the exact method");
1253 assert!(allow.contains(&Method::GET), "missing the wildcard method");
1254 }
1255 _ => panic!("expected 405"),
1256 }
1257 }
1258
1259 #[test]
1260 fn abandoned_branch_params_do_not_leak_into_the_wildcard() {
1261 let mut r = Router::new();
1262 {
1263 let mut b = RouteBuilder::new(&mut r);
1264 // Matches /u/7/edit structurally, but has no GET handler.
1265 b.post("/u/{id}/edit", |_c: Call| async { "edit" });
1266 b.get("/u/{rest...}", |_c: Call| async { "wild" });
1267 }
1268 match r.route(&Method::GET, "/u/7/edit", &probe("/u/7/edit")) {
1269 Match::Found { params, .. } => {
1270 assert_eq!(params.get("rest").unwrap(), "7/edit");
1271 assert!(
1272 !params.contains_key("id"),
1273 "stale `id` leaked from the abandoned walk"
1274 );
1275 }
1276 _ => panic!("the wildcard should have matched"),
1277 }
1278 }
1279
1280 #[test]
1281 fn an_encoded_separator_in_a_wildcard_tail_does_not_override_a_matching_sibling() {
1282 // The wildcard refuses this tail, but a sibling `{n}` matches the path
1283 // cleanly with `n = "a/b"` — that is what splitting before decoding is
1284 // for. The wildcard's opinion about its own tail must not be promoted
1285 // into a verdict on the whole request: only POST is registered at the
1286 // route that does match, so `405` is the honest answer and a `400`
1287 // would describe a request that is in fact perfectly well formed.
1288 let mut r = Router::new();
1289 {
1290 let mut b = RouteBuilder::new(&mut r);
1291 b.get("/files/{p...}", |_c: Call| async { "wild" });
1292 b.post("/files/{n}", |_c: Call| async { "one" });
1293 }
1294 match run(&r, Method::GET, "/files/a%2Fb") {
1295 Match::MethodNotAllowed { allow } => assert_eq!(allow, vec![Method::POST]),
1296 _ => panic!("expected the sibling param route to answer"),
1297 }
1298 }
1299
1300 #[test]
1301 fn unknown_path_is_not_found() {
1302 let r = build();
1303 assert!(matches!(run(&r, Method::GET, "/nope"), Match::NotFound));
1304 }
1305
1306 #[test]
1307 fn known_path_wrong_method_is_405() {
1308 let r = build();
1309 match run(&r, Method::DELETE, "/users/7") {
1310 Match::MethodNotAllowed { allow } => assert!(allow.contains(&Method::GET)),
1311 _ => panic!("expected 405"),
1312 }
1313 }
1314
1315 #[test]
1316 fn wildcard_captures_rest() {
1317 let r = build();
1318 match run(&r, Method::GET, "/files/a/b/c.txt") {
1319 Match::Found { params, .. } => assert_eq!(params.get("path").unwrap(), "a/b/c.txt"),
1320 _ => panic!("expected wildcard Found"),
1321 }
1322 }
1323
1324 // Regression guard for blocking issue #2: a pre-boxed `BoxHandler` must be
1325 // acceptable to the route builder methods, not only closures.
1326 #[tokio::test]
1327 async fn route_builder_accepts_boxed_handler() {
1328 let pre: BoxHandler = boxed((|_c: Call| async { "pre-boxed" }).into_handler());
1329 let mut r = Router::new();
1330 {
1331 let mut b = RouteBuilder::new(&mut r);
1332 b.get("/pre", pre);
1333 }
1334 match run(&r, Method::GET, "/pre") {
1335 Match::Found { handler, .. } => {
1336 let c = Call::new(
1337 Method::GET,
1338 "/pre".parse::<Uri>().unwrap(),
1339 HeaderMap::new(),
1340 Bytes::new(),
1341 );
1342 let res = handler.handle(c).await;
1343 assert_eq!(res.body, Bytes::from("pre-boxed"));
1344 }
1345 _ => panic!("expected Found"),
1346 }
1347 }
1348}