Skip to main content

rust_web_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    pub(crate) fn from_map(params: HashMap<String, String>) -> Self {
30        PathParams { params }
31    }
32
33    /// Returns the value for the named parameter, or `None` if absent.
34    pub fn get(&self, name: &str) -> Option<&str> {
35        self.params.get(name).map(String::as_str)
36    }
37}
38
39/// The host a request asked for: its `Host` header (HTTP/2 and HTTP/3 turn
40/// `:authority` into one), else the TLS SNI name, else `""`. Host comes first
41/// because browsers reuse one connection for every name its certificate covers
42/// (RFC 9113 §9.1.1), so SNI names the connection's first request, not this one.
43pub(crate) fn request_host<'a>(request: &'a Request, connection: &'a ConnectionInfo) -> &'a str {
44    request.headers.iter()
45        .find(|h| h.name.eq_ignore_ascii_case("host"))
46        .map(|h| h.value.as_ref())
47        .filter(|v: &&str| !v.is_empty())
48        .or(connection.sni_hostname.as_deref())
49        .unwrap_or("")
50}
51
52/// A route's `host` against the request's ([`request_host`]): ignoring ASCII
53/// case, and ignoring a port the route doesn't name (#50).
54pub(crate) fn host_matches(expected: &str, actual: &str) -> bool {
55    let actual = if expected.contains(':') {
56        actual
57    } else if let Some(rest) = actual.strip_prefix('[') {
58        // [IPv6]:port
59        rest.split(']').next().unwrap_or(rest)
60    } else {
61        actual.rsplit_once(':').map_or(actual, |(host, port)| if port.bytes().all(|b| b.is_ascii_digit()) { host } else { actual })
62    };
63    let expected = expected.trim_start_matches('[').trim_end_matches(']');
64    actual.eq_ignore_ascii_case(expected)
65}
66
67type HandlerFn =
68    Arc<dyn Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static>;
69
70#[derive(Clone)]
71struct Route {
72    method: String,
73    segments: Vec<Segment>,
74    handler: HandlerFn,
75}
76
77/// A path-based HTTP router with named parameter extraction.
78///
79/// Register routes with [`Router::get`], [`Router::post`], etc. Each handler
80/// receives the parsed [`PathParams`] alongside the raw [`Request`] and
81/// [`ConnectionInfo`]. Call [`Router::handle`] from inside a [`Controller`]
82/// or an [`Application::execute`] implementation.
83///
84/// # Example
85///
86/// ```rust,no_run
87/// use rust_web_server::router::{Router, PathParams};
88/// use rust_web_server::request::Request;
89/// use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
90/// use rust_web_server::range::Range;
91/// use rust_web_server::mime_type::MimeType;
92/// use rust_web_server::server::ConnectionInfo;
93/// use rust_web_server::core::New;
94///
95/// let router = Router::new()
96///     .get("/hello", |_req, _params, _conn| {
97///         let mut r = Response::new();
98///         r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
99///         r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
100///         r.content_range_list = vec![Range::get_content_range(b"hello".to_vec(), MimeType::TEXT_PLAIN.to_string())];
101///         r
102///     })
103///     .get("/users/:id", |_req, params, _conn| {
104///         let id = params.get("id").unwrap_or("unknown");
105///         let mut r = Response::new();
106///         r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
107///         r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
108///         r.content_range_list = vec![Range::get_content_range(
109///             format!("user {}", id).into_bytes(),
110///             MimeType::TEXT_PLAIN.to_string(),
111///         )];
112///         r
113///     });
114/// ```
115/// A registered route entry returned by [`Router::route_entries`].
116#[derive(Clone)]
117pub struct RouteInfo {
118    pub method: String,
119    pub pattern: String,
120}
121
122#[derive(Clone)]
123pub struct Router {
124    routes: Vec<Route>,
125    /// When set, `handle()` only matches if the request's SNI hostname (or
126    /// `Host` header for plain HTTP) equals this value.
127    host: Option<String>,
128}
129
130impl Router {
131    pub fn new() -> Self {
132        Router { routes: Vec::new(), host: None }
133    }
134
135    /// Restrict this router to requests for `host`: their `Host` header
136    /// (`:authority` over HTTP/2 and HTTP/3), else the TLS SNI name, ignoring
137    /// case and a port `host` doesn't name.  Call before registering routes.
138    pub fn with_host(mut self, host: &str) -> Self {
139        self.host = Some(host.to_string());
140        self
141    }
142
143    /// Register a `GET` handler for `pattern`.
144    pub fn get<F>(self, pattern: &str, handler: F) -> Self
145    where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
146        self.add("GET", pattern, handler)
147    }
148
149    /// Serve WebSocket connections at `pattern`: an upgrade request's
150    /// connection goes to `handler` with the route's parameters, anything
151    /// else gets `426 Upgrade Required` (see
152    /// [`WebSocket::upgrade`](crate::websocket::WebSocket::upgrade)).
153    ///
154    /// ```rust,no_run
155    /// use rust_web_server::router::Router;
156    /// use rust_web_server::websocket::Message;
157    ///
158    /// let router = Router::new().websocket("/rooms/:room", |mut ws, params| {
159    ///     let room = params.get("room").unwrap_or("lobby").to_string();
160    ///     while let Ok(Some(Message::Text(text))) = ws.recv() {
161    ///         let _ = ws.send_text(&format!("{room}: {text}"));
162    ///     }
163    /// });
164    /// ```
165    #[cfg(not(target_arch = "wasm32"))]
166    pub fn websocket<F>(self, pattern: &str, handler: F) -> Self
167    where
168        F: Fn(crate::websocket::WebSocketConnection, PathParams) + Send + Sync + 'static,
169    {
170        let handler = Arc::new(handler);
171        self.get(pattern, move |req, params, _| {
172            let (handler, params) = (Arc::clone(&handler), params.clone());
173            crate::websocket::WebSocket::upgrade(req, move |ws| handler(ws, params))
174        })
175    }
176
177    /// Register a `POST` handler for `pattern`.
178    pub fn post<F>(self, pattern: &str, handler: F) -> Self
179    where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
180        self.add("POST", pattern, handler)
181    }
182
183    /// Register a `PUT` handler for `pattern`.
184    pub fn put<F>(self, pattern: &str, handler: F) -> Self
185    where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
186        self.add("PUT", pattern, handler)
187    }
188
189    /// Register a `PATCH` handler for `pattern`.
190    pub fn patch<F>(self, pattern: &str, handler: F) -> Self
191    where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
192        self.add("PATCH", pattern, handler)
193    }
194
195    /// Register a `DELETE` handler for `pattern`.
196    pub fn delete<F>(self, pattern: &str, handler: F) -> Self
197    where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
198        self.add("DELETE", pattern, handler)
199    }
200
201    pub(crate) fn add<F>(mut self, method: &str, pattern: &str, handler: F) -> Self
202    where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
203        self.routes.push(Route {
204            method: method.to_string(),
205            segments: matcher::parse_pattern(pattern),
206            handler: Arc::new(handler),
207        });
208        self
209    }
210
211    /// Return a snapshot of all registered routes as `(method, pattern)` pairs.
212    ///
213    /// Patterns are reconstructed from parsed segments, so the output exactly
214    /// matches what was passed to `.get()`, `.post()`, etc. at registration time.
215    pub fn route_entries(&self) -> Vec<RouteInfo> {
216        self.routes.iter().map(|r| RouteInfo {
217            method: r.method.clone(),
218            pattern: matcher::segments_to_pattern(&r.segments),
219        }).collect()
220    }
221
222    /// Try to match `request` against registered routes in registration order.
223    ///
224    /// Returns `Some(response)` on the first match, `None` if no route matches.
225    /// The query string is stripped before matching; only the path is used.
226    ///
227    /// When `.with_host()` is set, this returns `None` immediately unless the
228    /// request's host (`Host` header, else SNI) matches.
229    pub fn handle(&self, request: &Request, connection: &ConnectionInfo) -> Option<Response> {
230        let (route, params) = self.find(request, connection)?;
231        Some((route.handler)(request, &params, connection))
232    }
233
234    /// `true` if [`handle`](Self::handle) would run a handler for `request`
235    /// (method, path and host all match), without running it.
236    pub fn matches(&self, request: &Request, connection: &ConnectionInfo) -> bool {
237        self.find(request, connection).is_some()
238    }
239
240    fn find(&self, request: &Request, connection: &ConnectionInfo) -> Option<(&Route, PathParams)> {
241        if let Some(required_host) = &self.host {
242            if !host_matches(required_host, request_host(request, connection)) {
243                return None;
244            }
245        }
246
247        let path = request.request_uri.split('?').next().unwrap_or(&request.request_uri);
248        let path_segs: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
249
250        for route in &self.routes {
251            if route.method != request.method {
252                continue;
253            }
254            if let Some(params) = matcher::try_match(&route.segments, &path_segs) {
255                return Some((route, PathParams::from_map(params)));
256            }
257        }
258        None
259    }
260}