Skip to main content

mini_serve/
router.rs

1use std::collections::HashMap;
2
3use hyper::Method;
4
5use crate::handler::Handler;
6
7/// Extracted path parameters from a matched route.
8///
9/// Contains a map of parameter names to their decoded string values
10/// (e.g., `id` → `"42"` from a route `/items/:id`). Populated by the router
11/// and stored in request extensions; extract via `path_params::<T>(req)`.
12#[derive(Clone, Debug, Default)]
13pub struct PathParams(pub HashMap<String, String>);
14
15/// Parsed query parameters from the request URL.
16///
17/// Contains a map of query parameter names to their values, with `+` decoded
18/// as space and percent-encoded sequences decoded. Populated by the app and
19/// stored in request extensions; extract via `query_params(&req)`, which reads a
20/// request with no query string as one with no parameters.
21#[derive(Clone, Debug, Default)]
22pub struct QueryParams(pub HashMap<String, String>);
23
24/// A trie-based HTTP router for matching requests to handlers.
25///
26/// Routes are matched by method and path, with support for dynamic path parameters
27/// (`:name`) and wildcards (`*`). Each request is matched in a single trie traversal.
28#[derive(Default)]
29pub struct Router<S> {
30	root: Node<S>,
31}
32
33struct Node<S> {
34	segment:    String,
35	param_name: String,
36	is_wildcard: bool,
37	/// Methods registered at this node, in registration order.
38	///
39	/// A `HashMap` here paid SipHash — a hasher chosen to make *insertion* of
40	/// attacker-chosen colliding keys expensive — on a map whose keys are the handful
41	/// of `Method` constants the router itself registered. An attacker controls only
42	/// the lookup key, never a key in the map, so there is no HashDoS primitive to
43	/// defend against, and a scan bounded by the number of methods on one route beats
44	/// hashing outright at this size. If handler registration ever becomes reachable
45	/// from request data, this trade has to be revisited.
46	handlers:   Vec<(Method, Handler<S>)>,
47	children:   Vec<Node<S>>,
48}
49
50impl<S> Default for Node<S> {
51	fn default() -> Self {
52		Node {
53			segment:    String::new(),
54			param_name: String::new(),
55			is_wildcard: false,
56			handlers:   Vec::new(),
57			children:   Vec::new(),
58		}
59	}
60}
61
62impl<S: Send + Sync + 'static> Router<S> {
63	/// Create a new empty router.
64	pub fn new() -> Self {
65		Router { root: Node::default() }
66	}
67
68	/// Register a handler for the given method and path.
69	///
70	/// Paths may contain static segments, dynamic parameters (`:name`), and a
71	/// wildcard (`*`) to match everything. For example: `/users/:id` or `/api/*`.
72	pub fn insert(&mut self, method: Method, path: &str, handler: Handler<S>) {
73		let segments = split_path(path);
74		let mut node = &mut self.root;
75
76		for seg in segments {
77			if seg == "*" {
78				if let Some(idx) = node.children.iter().position(|c| c.is_wildcard) {
79					node = &mut node.children[idx];
80				} else {
81					node.children.push(Node {
82						segment:    "*".to_string(),
83						param_name: String::new(),
84						is_wildcard: true,
85						handlers:   Vec::new(),
86						children:   Vec::new(),
87					});
88					node = node.children.last_mut().unwrap();
89				}
90			} else if let Some(param_name) = seg.strip_prefix(':') {
91				if let Some(idx) = node.children.iter().position(|c| c.param_name == param_name) {
92					node = &mut node.children[idx];
93				} else {
94					node.children.push(Node {
95						segment:    seg.to_string(),
96						param_name: param_name.to_string(),
97						is_wildcard: false,
98						handlers:   Vec::new(),
99						children:   Vec::new(),
100					});
101					node = node.children.last_mut().unwrap();
102				}
103			} else {
104				if let Some(idx) = node.children.iter().position(|c| c.segment == seg) {
105					node = &mut node.children[idx];
106				} else {
107					node.children.push(Node {
108						segment:    seg.to_string(),
109						param_name: String::new(),
110						is_wildcard: false,
111						handlers:   Vec::new(),
112						children:   Vec::new(),
113					});
114					node = node.children.last_mut().unwrap();
115				}
116			}
117		}
118
119		// Replace rather than push on re-registration, matching what `HashMap::insert`
120		// did: registering the same method twice on one path must leave one handler,
121		// not shadow the first with an unreachable second.
122		if let Some(slot) = node.handlers.iter_mut().find(|(m, _)| *m == method) {
123			slot.1 = handler;
124		} else {
125			node.handlers.push((method, handler));
126		}
127	}
128
129	/// Find and return the handler for the given method and path, along with extracted parameters.
130	///
131	/// Returns `None` if no route matches. Path parameters are percent-decoded and
132	/// included in the returned `PathParams`.
133	pub fn match_route<'a>(
134		&'a self,
135		method: &Method,
136		path: &str,
137	) -> Option<(&'a Handler<S>, PathParams)> {
138		let segments = split_path(path);
139		let mut params = PathParams::default();
140		let node = Self::find_node(&self.root, &segments, 0, method, &mut params)?;
141		node.handlers.iter().find(|(m, _)| m == method).map(|(_, h)| (h, params))
142	}
143
144	/// List all HTTP methods that have handlers registered for the given path.
145	///
146	/// Returns an empty vector if the path has no registered handlers.
147	pub fn allowed_methods(&self, path: &str) -> Vec<Method> {
148		let segments = split_path(path);
149		let mut methods = std::collections::HashSet::new();
150		let mut params = PathParams::default();
151		Self::collect_allowed_methods(&self.root, &segments, 0, &mut params, &mut methods);
152		methods.into_iter().collect()
153	}
154
155	/// Check whether any handler is registered for the given path.
156	pub fn path_exists(&self, path: &str) -> bool {
157		!self.allowed_methods(path).is_empty()
158	}
159
160	/// Recursive backtracking search for a trie node that is both
161	/// path-complete *and* has a handler for `method`.
162	///
163	/// Tries children in precedence order: static → param → wildcard.
164	/// Backtracks to the next-choice child on either kind of dead end: no
165	/// path match, or a path-complete node with no handler for `method` (so
166	/// e.g. a registered `POST /users/new` doesn't shadow `GET /users/:id`
167	/// for a `GET /users/new` request — the static branch is path-complete
168	/// but lacks GET, so the search falls back to the param branch).
169	///
170	/// Optimization: borrows params through static segments without cloning,
171	/// only cloning before modifying for param branches. On backtrack,
172	/// truncates params to remove any additions made in failed attempts.
173	fn find_node<'a>(
174		node: &'a Node<S>,
175		segments: &[String],
176		idx: usize,
177		method: &Method,
178		params: &mut PathParams,
179	) -> Option<&'a Node<S>> {
180		if idx == segments.len() {
181			return if node.handlers.iter().any(|(m, _)| m == method) { Some(node) } else { None };
182		}
183
184		let seg = &segments[idx];
185
186		for child in &node.children {
187			if !child.is_wildcard && child.param_name.is_empty() && child.segment == *seg {
188				// Static segment: pass params through without cloning.
189				if let Some(found) = Self::find_node(child, segments, idx + 1, method, params) {
190					return Some(found);
191				}
192			}
193		}
194
195		for child in &node.children {
196			if !child.is_wildcard && !child.param_name.is_empty() {
197				// Param segment: clone before modifying for backtrack safety.
198				let mut p = params.clone();
199				p.0.insert(child.param_name.clone(), seg.clone());
200				if let Some(found) = Self::find_node(child, segments, idx + 1, method, &mut p) {
201					*params = p;
202					return Some(found);
203				}
204				// Backtrack: remove the param we added in this failed attempt.
205				params.0.remove(&child.param_name);
206			}
207		}
208
209		for child in &node.children {
210			if child.is_wildcard && child.handlers.iter().any(|(m, _)| m == method) {
211				params.0.insert("*".to_string(), segments[idx..].join("/"));
212				return Some(child);
213			}
214		}
215
216		None
217	}
218
219	/// Exhaustive counterpart to [`find_node`] for computing the `Allow`
220	/// header: unions handler methods across *every* path-complete node
221	/// reachable, not just the first one the static→param→wildcard
222	/// precedence would settle on. Necessary because, per the same
223	/// ambiguity `find_node` backtracks around, more than one branch can be
224	/// path-complete for a given request path.
225	fn collect_allowed_methods(
226		node: &Node<S>,
227		segments: &[String],
228		idx: usize,
229		params: &mut PathParams,
230		methods: &mut std::collections::HashSet<Method>,
231	) {
232		if idx == segments.len() {
233			methods.extend(node.handlers.iter().map(|(m, _)| m.clone()));
234			return;
235		}
236
237		let seg = &segments[idx];
238
239		for child in &node.children {
240			if !child.is_wildcard && child.param_name.is_empty() && child.segment == *seg {
241				let mut p = params.clone();
242				Self::collect_allowed_methods(child, segments, idx + 1, &mut p, methods);
243			}
244		}
245
246		for child in &node.children {
247			if !child.is_wildcard && !child.param_name.is_empty() {
248				let mut p = params.clone();
249				p.0.insert(child.param_name.clone(), seg.clone());
250				Self::collect_allowed_methods(child, segments, idx + 1, &mut p, methods);
251			}
252		}
253
254		for child in &node.children {
255			if child.is_wildcard {
256				methods.extend(child.handlers.iter().map(|(m, _)| m.clone()));
257			}
258		}
259	}
260}
261
262fn split_path(path: &str) -> Vec<String> {
263	path.trim_start_matches('/')
264		.split('/')
265		.filter(|s| !s.is_empty())
266		.map(|s| percent_encoding::percent_decode_str(s).decode_utf8_lossy().into_owned())
267		.collect()
268}
269
270#[cfg(test)]
271#[path = "../tests/unit/router.rs"]
272mod tests;
273
274#[cfg(test)]
275#[path = "../tests/unit/router_properties.rs"]
276mod property_tests;