Skip to main content

rws_server/router/
mod.rs

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