Skip to main content

rws_server/router/
mod.rs

1pub(crate) mod matcher;
2pub(crate) mod names;
3#[cfg(test)]
4mod tests;
5
6use std::collections::HashMap;
7use std::sync::Arc;
8
9use crate::request::Request;
10use crate::response::Response;
11use crate::server::ConnectionInfo;
12use matcher::Segment;
13
14pub use names::{build_path, name_route, named_pattern, named_routes, url_for};
15
16/// Named path-segment values extracted from a matched route pattern.
17///
18/// Given the pattern `/users/:id/posts/:post_id` matched against
19/// `/users/42/posts/7`, `params.get("id")` returns `Some("42")` and
20/// `params.get("post_id")` returns `Some("7")`.
21///
22/// Wildcard segments (`*name`) capture everything after the prefix:
23/// `/files/*path` matched against `/files/a/b/c` gives `path = "a/b/c"`.
24#[derive(Clone)]
25pub struct PathParams {
26    params: HashMap<String, String>,
27}
28
29impl PathParams {
30    /// Build a `PathParams` from an existing map โ€” used to adapt
31    /// [`matcher::try_match`]'s output for both `Router` and `AsyncAppWithState`.
32    #[doc(hidden)]
33    pub fn from_map(params: HashMap<String, String>) -> Self {
34        PathParams { params }
35    }
36
37    /// Returns the value for the named parameter, or `None` if absent.
38    pub fn get(&self, name: &str) -> Option<&str> {
39        self.params.get(name).map(String::as_str)
40    }
41}
42
43/// The host a request asked for: its `Host` header (HTTP/2 and HTTP/3 turn
44/// `:authority` into one), else the TLS SNI name, else `""`. Host comes first
45/// because browsers reuse one connection for every name its certificate covers
46/// (RFC 9113 ยง9.1.1), so SNI names the connection's first request, not this one.
47#[doc(hidden)]
48pub fn request_host<'a>(request: &'a Request, connection: &'a ConnectionInfo) -> &'a str {
49    request.headers.iter()
50        .find(|h| h.name.eq_ignore_ascii_case("host"))
51        .map(|h| h.value.as_ref())
52        .filter(|v: &&str| !v.is_empty())
53        .or(connection.sni_hostname.as_deref())
54        .unwrap_or("")
55}
56
57/// A route's `host` against the request's ([`request_host`]): ignoring ASCII
58/// case, and ignoring a port the route doesn't name (#50).
59#[doc(hidden)]
60pub fn host_matches(expected: &str, actual: &str) -> bool {
61    let actual = if expected.contains(':') {
62        actual
63    } else if let Some(rest) = actual.strip_prefix('[') {
64        // [IPv6]:port
65        rest.split(']').next().unwrap_or(rest)
66    } else {
67        actual.rsplit_once(':').map_or(actual, |(host, port)| if port.bytes().all(|b| b.is_ascii_digit()) { host } else { actual })
68    };
69    let expected = expected.trim_start_matches('[').trim_end_matches(']');
70    actual.eq_ignore_ascii_case(expected)
71}
72
73type HandlerFn =
74    Arc<dyn Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static>;
75
76#[derive(Clone)]
77struct Route {
78    method: String,
79    segments: Vec<Segment>,
80    handler: HandlerFn,
81}
82
83/// A path-based HTTP router with named parameter extraction.
84///
85/// Register routes with [`Router::get`], [`Router::post`], etc. Each handler
86/// receives the parsed [`PathParams`] alongside the raw [`Request`] and
87/// [`ConnectionInfo`]. Call [`Router::handle`] from inside a [`Controller`]
88/// or an [`Application::execute`] implementation.
89///
90/// # Example
91///
92/// ```rust,no_run
93/// # extern crate rws_server as rust_web_server;
94/// use rust_web_server::router::{Router, PathParams};
95/// use rust_web_server::request::Request;
96/// use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
97/// use rust_web_server::range::Range;
98/// use rust_web_server::mime_type::MimeType;
99/// use rust_web_server::server::ConnectionInfo;
100/// use rust_web_server::core::New;
101///
102/// let router = Router::new()
103///     .get("/hello", |_req, _params, _conn| {
104///         let mut r = Response::new();
105///         r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
106///         r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
107///         r.content_range_list = vec![Range::get_content_range(b"hello".to_vec(), MimeType::TEXT_PLAIN.to_string())];
108///         r
109///     })
110///     .get("/users/:id", |_req, params, _conn| {
111///         let id = params.get("id").unwrap_or("unknown");
112///         let mut r = Response::new();
113///         r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
114///         r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
115///         r.content_range_list = vec![Range::get_content_range(
116///             format!("user {}", id).into_bytes(),
117///             MimeType::TEXT_PLAIN.to_string(),
118///         )];
119///         r
120///     });
121/// ```
122/// A registered route entry returned by [`Router::route_entries`].
123#[derive(Clone)]
124pub struct RouteInfo {
125    pub method: String,
126    pub pattern: String,
127}
128
129/// A route that never runs because one registered before it, with the same
130/// method, matches every path it does โ€” what
131/// [`Router::route_conflicts`] (and `AppWithState`/`AsyncAppWithState`'s)
132/// report (18.28.0). Patterns are in the `:name`/`*name` form.
133#[derive(Clone, Debug, PartialEq, Eq)]
134pub struct RouteConflict {
135    pub method: String,
136    /// The route that never runs.
137    pub pattern: String,
138    /// The earlier route that answers its requests.
139    pub earlier: String,
140    pub kind: RouteConflictKind,
141}
142
143/// How a [`RouteConflict`]'s earlier route hides the later one.
144#[derive(Clone, Copy, Debug, PartialEq, Eq)]
145pub enum RouteConflictKind {
146    /// The same pattern, parameter names aside: registered twice.
147    Duplicate,
148    /// The earlier pattern is more general (a parameter or wildcard where
149    /// the later one has a literal).
150    Shadowed,
151}
152
153impl std::fmt::Display for RouteConflict {
154    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155        match self.kind {
156            RouteConflictKind::Duplicate => write!(
157                f,
158                "route {} {} is registered twice (first as {} {}); the second never runs",
159                self.method, self.pattern, self.method, self.earlier
160            ),
161            RouteConflictKind::Shadowed => write!(
162                f,
163                "route {} {} never runs: {} {}, registered before it, matches every path it does",
164                self.method, self.pattern, self.method, self.earlier
165            ),
166        }
167    }
168}
169
170impl RouteConflict {
171    /// Print the conflict to stderr, once per process for each distinct one.
172    #[doc(hidden)]
173    pub fn warn_once(&self) {
174        use std::collections::HashSet;
175        use std::sync::{Mutex, OnceLock};
176        static SEEN: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
177        let message = self.to_string();
178        let fresh = SEEN
179            .get_or_init(|| Mutex::new(HashSet::new()))
180            .lock()
181            .map(|mut seen| seen.insert(message.clone()))
182            .unwrap_or(false);
183        if fresh {
184            eprintln!("rws: warning: {message}");
185        }
186    }
187}
188
189/// The conflicts among `routes`, in registration order.
190#[doc(hidden)]
191pub fn conflicts<'a>(routes: impl Iterator<Item = (&'a str, &'a [Segment])>) -> Vec<RouteConflict> {
192    let all: Vec<(&str, &[Segment])> = routes.collect();
193    (1..all.len())
194        .filter_map(|i| conflict_with(all[..i].iter().copied(), all[i].0, all[i].1))
195        .collect()
196}
197
198/// The first of `earlier` that hides a route `method` `segments` added after them.
199pub(crate) fn conflict_with<'a>(
200    earlier: impl Iterator<Item = (&'a str, &'a [Segment])>,
201    method: &str,
202    segments: &[Segment],
203) -> Option<RouteConflict> {
204    if matcher::never_matches(segments) {
205        return None;
206    }
207    for (m, segs) in earlier {
208        if m != method {
209            continue;
210        }
211        let kind = if matcher::equivalent(segs, segments) {
212            RouteConflictKind::Duplicate
213        } else if matcher::covers(segs, segments) {
214            RouteConflictKind::Shadowed
215        } else {
216            continue;
217        };
218        return Some(RouteConflict {
219            method: method.to_string(),
220            pattern: matcher::segments_to_pattern(segments),
221            earlier: matcher::segments_to_pattern(segs),
222            kind,
223        });
224    }
225    None
226}
227
228#[derive(Clone)]
229pub struct Router {
230    routes: Vec<Route>,
231    /// When set, `handle()` only matches if the request's SNI hostname (or
232    /// `Host` header for plain HTTP) equals this value.
233    host: Option<String>,
234}
235
236impl Router {
237    pub fn new() -> Self {
238        Router { routes: Vec::new(), host: None }
239    }
240
241    /// Restrict this router to requests for `host`: their `Host` header
242    /// (`:authority` over HTTP/2 and HTTP/3), else the TLS SNI name, ignoring
243    /// case and a port `host` doesn't name.  Call before registering routes.
244    pub fn with_host(mut self, host: &str) -> Self {
245        self.host = Some(host.to_string());
246        self
247    }
248
249    /// Register a `GET` handler for `pattern`.
250    pub fn get<F>(self, pattern: &str, handler: F) -> Self
251    where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
252        self.add("GET", pattern, handler)
253    }
254
255    /// Serve WebSocket connections at `pattern`: an upgrade request's
256    /// connection goes to `handler` with the route's parameters, anything
257    /// else gets `426 Upgrade Required` (see
258    /// [`WebSocket::upgrade`](crate::websocket::WebSocket::upgrade)).
259    ///
260    /// ```rust,no_run
261    /// # extern crate rws_server as rust_web_server;
262    /// use rust_web_server::router::Router;
263    /// use rust_web_server::websocket::Message;
264    ///
265    /// let router = Router::new().websocket("/rooms/:room", |mut ws, params| {
266    ///     let room = params.get("room").unwrap_or("lobby").to_string();
267    ///     while let Ok(Some(Message::Text(text))) = ws.recv() {
268    ///         let _ = ws.send_text(&format!("{room}: {text}"));
269    ///     }
270    /// });
271    /// ```
272    #[cfg(not(target_arch = "wasm32"))]
273    pub fn websocket<F>(self, pattern: &str, handler: F) -> Self
274    where
275        F: Fn(crate::websocket::WebSocketConnection, PathParams) + Send + Sync + 'static,
276    {
277        let handler = Arc::new(handler);
278        self.get(pattern, move |req, params, _| {
279            let (handler, params) = (Arc::clone(&handler), params.clone());
280            crate::websocket::WebSocket::upgrade(req, move |ws| handler(ws, params))
281        })
282    }
283
284    /// Register a `POST` handler for `pattern`.
285    pub fn post<F>(self, pattern: &str, handler: F) -> Self
286    where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
287        self.add("POST", pattern, handler)
288    }
289
290    /// Register a `PUT` handler for `pattern`.
291    pub fn put<F>(self, pattern: &str, handler: F) -> Self
292    where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
293        self.add("PUT", pattern, handler)
294    }
295
296    /// Register a `PATCH` handler for `pattern`.
297    pub fn patch<F>(self, pattern: &str, handler: F) -> Self
298    where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
299        self.add("PATCH", pattern, handler)
300    }
301
302    /// Register a `DELETE` handler for `pattern`.
303    pub fn delete<F>(self, pattern: &str, handler: F) -> Self
304    where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
305        self.add("DELETE", pattern, handler)
306    }
307
308    #[doc(hidden)]
309    pub fn add<F>(self, method: &str, pattern: &str, handler: F) -> Self
310    where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
311        let router = self.add_unchecked(method, pattern, handler);
312        if let Some(conflict) = router.last_route_conflict() {
313            conflict.warn_once();
314        }
315        router
316    }
317
318    /// [`add`](Self::add) without the conflict warning, for routers that
319    /// are only matched (`AppWithState::stream_body`).
320    pub(crate) fn add_unchecked<F>(mut self, method: &str, pattern: &str, handler: F) -> Self
321    where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
322        self.routes.push(Route {
323            method: method.to_string(),
324            segments: matcher::parse_pattern(pattern),
325            handler: Arc::new(handler),
326        });
327        self
328    }
329
330    /// Register a typed handler (18.28.0): a function with extractor
331    /// arguments annotated `#[get("/path")]` & co., whose method and pattern
332    /// come from the attribute. A router has no state, so `State<S>` and
333    /// `&S` arguments aren't available here โ€” use
334    /// [`AppWithState::route`](crate::state::AppWithState::route).
335    ///
336    /// ```rust,ignore
337    /// #[get("/users/{id}")]
338    /// fn user(Path(id): Path<u64>) -> String { format!("user {id}") }
339    ///
340    /// let router = Router::new().route(user);
341    /// ```
342    pub fn route<H: crate::handler::Handler<()>>(self, handler: H) -> Self {
343        let unit = Arc::new(());
344        self.add(H::METHOD, H::PATTERN, move |request, params, connection| {
345            handler.handle(request, params, connection, &unit)
346        })
347    }
348
349    /// Routes that never run (18.28.0): registered after a route with the
350    /// same method whose pattern is equivalent (`/users/:id` and
351    /// `/users/{uid}`) or matches every path they do (`/users/:id` before
352    /// `/users/me`). Routes are tried in order, so the earlier one always
353    /// wins. Each is also printed once to stderr when it is registered.
354    pub fn route_conflicts(&self) -> Vec<RouteConflict> {
355        conflicts(self.routes.iter().map(|r| (r.method.as_str(), r.segments.as_slice())))
356    }
357
358    /// The pattern of the route registered last (`:name` form).
359    pub(crate) fn last_pattern(&self) -> Option<String> {
360        self.routes.last().map(|r| matcher::segments_to_pattern(&r.segments))
361    }
362
363    fn last_route_conflict(&self) -> Option<RouteConflict> {
364        let (last, earlier) = self.routes.split_last()?;
365        conflict_with(earlier.iter().map(|r| (r.method.as_str(), r.segments.as_slice())), &last.method, &last.segments)
366    }
367
368    /// Return a snapshot of all registered routes as `(method, pattern)` pairs.
369    ///
370    /// Patterns are reconstructed from parsed segments, so the output exactly
371    /// matches what was passed to `.get()`, `.post()`, etc. at registration time.
372    pub fn route_entries(&self) -> Vec<RouteInfo> {
373        self.routes.iter().map(|r| RouteInfo {
374            method: r.method.clone(),
375            pattern: matcher::segments_to_pattern(&r.segments),
376        }).collect()
377    }
378
379    /// Try to match `request` against registered routes in registration order.
380    ///
381    /// Returns `Some(response)` on the first match, `None` if no route matches.
382    /// The query string is stripped before matching; only the path is used.
383    ///
384    /// When `.with_host()` is set, this returns `None` immediately unless the
385    /// request's host (`Host` header, else SNI) matches.
386    pub fn handle(&self, request: &Request, connection: &ConnectionInfo) -> Option<Response> {
387        let (route, params) = self.find(request, connection)?;
388        Some((route.handler)(request, &params, connection))
389    }
390
391    /// `true` if [`handle`](Self::handle) would run a handler for `request`
392    /// (method, path and host all match), without running it.
393    pub fn matches(&self, request: &Request, connection: &ConnectionInfo) -> bool {
394        self.find(request, connection).is_some()
395    }
396
397    fn find(&self, request: &Request, connection: &ConnectionInfo) -> Option<(&Route, PathParams)> {
398        if let Some(required_host) = &self.host {
399            if !host_matches(required_host, request_host(request, connection)) {
400                return None;
401            }
402        }
403
404        let path = request.request_uri.split('?').next().unwrap_or(&request.request_uri);
405        let path_segs: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
406
407        for route in &self.routes {
408            if route.method != request.method {
409                continue;
410            }
411            if let Some(params) = matcher::try_match(&route.segments, &path_segs) {
412                return Some((route, PathParams::from_map(params)));
413            }
414        }
415        None
416    }
417}