rocket_community/router/
matcher.rs

1use crate::http::Status;
2use crate::route::Color;
3use crate::router::Collide;
4use crate::{Catcher, Request, Route};
5
6impl Route {
7    /// Returns `true` if `self` matches `request`.
8    ///
9    /// A [_match_](Route#routing) occurs when:
10    ///
11    ///   * The route's method matches that of the incoming request.
12    ///   * Either the route has no format _or_:
13    ///     - If the route's method supports a payload, the request's
14    ///       `Content-Type` is [fully specified] and [collides with] the
15    ///       route's format.
16    ///     - If the route's method does not support a payload, the request
17    ///       either has no `Accept` header or it [collides with] with the
18    ///       route's format.
19    ///   * All static segments in the route's URI match the corresponding
20    ///     components in the same position in the incoming request URI.
21    ///   * The route URI has no query part _or_ all static segments in the
22    ///     route's query string are in the request query string, though in any
23    ///     position.
24    ///
25    /// [fully specified]: crate::http::MediaType::specificity()
26    /// [collides with]: Route::collides_with()
27    ///
28    /// For a request to be routed to a particular route, that route must both
29    /// `match` _and_ have the highest precedence among all matching routes for
30    /// that request. In other words, a `match` is a necessary but insufficient
31    /// condition to determine if a route will handle a particular request.
32    ///
33    /// The precedence of a route is determined by its rank. Routes with lower
34    /// ranks have higher precedence. [By default](Route#default-ranking), more
35    /// specific routes are assigned a lower ranking.
36    ///
37    /// # Example
38    ///
39    /// ```rust
40    /// # extern crate rocket_community as rocket;
41    ///
42    /// use rocket::Route;
43    /// use rocket::http::Method;
44    /// # use rocket::local::blocking::Client;
45    /// # use rocket::route::dummy_handler as handler;
46    ///
47    /// // This route handles GET requests to `/<hello>`.
48    /// let a = Route::new(Method::Get, "/<hello>", handler);
49    ///
50    /// // This route handles GET requests to `/здрасти`.
51    /// let b = Route::new(Method::Get, "/здрасти", handler);
52    ///
53    /// # let client = Client::debug(rocket::build()).unwrap();
54    /// // Let's say `request` is `GET /hello`. The request matches only `a`:
55    /// let request = client.get("/hello");
56    /// # let request = request.inner();
57    /// assert!(a.matches(&request));
58    /// assert!(!b.matches(&request));
59    ///
60    /// // Now `request` is `GET /здрасти`. It matches both `a` and `b`:
61    /// let request = client.get("/здрасти");
62    /// # let request = request.inner();
63    /// assert!(a.matches(&request));
64    /// assert!(b.matches(&request));
65    ///
66    /// // But `b` is more specific, so it has lower rank (higher precedence)
67    /// // by default, so Rocket would route the request to `b`, not `a`.
68    /// assert!(b.rank < a.rank);
69    /// ```
70    #[tracing::instrument(level = "trace", name = "matching", skip_all, ret)]
71    pub fn matches(&self, request: &Request<'_>) -> bool {
72        methods_match(self, request)
73            && paths_match(self, request)
74            && queries_match(self, request)
75            && formats_match(self, request)
76    }
77}
78
79impl Catcher {
80    /// Returns `true` if `self` matches errors with `status` that occurred
81    /// during `request`.
82    ///
83    /// A [_match_](Catcher#routing) between a `Catcher` and a (`Status`,
84    /// `&Request`) pair occurs when:
85    ///
86    ///   * The catcher has the same [code](Catcher::code) as
87    ///     [`status`](Status::code) _or_ is `default`.
88    ///   * The catcher's [base](Catcher::base()) is a prefix of the `request`'s
89    ///     [normalized](crate::http::uri::Origin#normalization) URI.
90    ///
91    /// For an error arising from a request to be routed to a particular
92    /// catcher, that catcher must both `match` _and_ have higher precedence
93    /// than any other catcher that matches. In other words, a `match` is a
94    /// necessary but insufficient condition to determine if a catcher will
95    /// handle a particular error.
96    ///
97    /// The precedence of a catcher is determined by:
98    ///
99    ///   1. The number of _complete_ segments in the catcher's `base`.
100    ///   2. Whether the catcher is `default` or not.
101    ///
102    /// Non-default routes, and routes with more complete segments in their
103    /// base, have higher precedence.
104    ///
105    /// # Example
106    ///
107    /// ```rust
108    /// # extern crate rocket_community as rocket;
109    ///
110    /// use rocket::Catcher;
111    /// use rocket::http::Status;
112    /// # use rocket::local::blocking::Client;
113    /// # use rocket::catcher::dummy_handler as handler;
114    ///
115    /// // This catcher handles 404 errors with a base of `/`.
116    /// let a = Catcher::new(404, handler);
117    ///
118    /// // This catcher handles 404 errors with a base of `/bar`.
119    /// let b = a.clone().map_base(|_| format!("/bar")).unwrap();
120    ///
121    /// # let client = Client::debug(rocket::build()).unwrap();
122    /// // Let's say `request` is `GET /` that 404s. The error matches only `a`:
123    /// let request = client.get("/");
124    /// # let request = request.inner();
125    /// assert!(a.matches(Status::NotFound, &request));
126    /// assert!(!b.matches(Status::NotFound, &request));
127    ///
128    /// // Now `request` is a 404 `GET /bar`. The error matches `a` and `b`:
129    /// let request = client.get("/bar");
130    /// # let request = request.inner();
131    /// assert!(a.matches(Status::NotFound, &request));
132    /// assert!(b.matches(Status::NotFound, &request));
133    ///
134    /// // Note that because `b`'s base' has more complete segments that `a's,
135    /// // Rocket would route the error to `b`, not `a`, even though both match.
136    /// let a_count = a.base().segments().filter(|s| !s.is_empty()).count();
137    /// let b_count = b.base().segments().filter(|s| !s.is_empty()).count();
138    /// assert!(b_count > a_count);
139    /// ```
140    pub fn matches(&self, status: Status, request: &Request<'_>) -> bool {
141        self.code.is_none_or(|code| code == status.code)
142            && self
143                .base()
144                .segments()
145                .prefix_of(request.uri().path().segments())
146    }
147}
148
149fn methods_match(route: &Route, req: &Request<'_>) -> bool {
150    trace!(?route.method, request.method = %req.method());
151    route.method.is_none_or(|method| method == req.method())
152}
153
154fn paths_match(route: &Route, req: &Request<'_>) -> bool {
155    trace!(route.uri = %route.uri, request.uri = %req.uri());
156    let route_segments = &route.uri.metadata.uri_segments;
157    let req_segments = req.uri().path().segments();
158
159    // A route can never have more segments than a request. Recall that a
160    // trailing slash is considering a segment, albeit empty.
161    if route_segments.len() > req_segments.num() {
162        return false;
163    }
164
165    // requests with longer paths only match if we have dynamic trail (<a..>).
166    if req_segments.num() > route_segments.len() && !route.uri.metadata.dynamic_trail {
167        return false;
168    }
169
170    // We've checked everything beyond the zip of their lengths already.
171    for (route_seg, req_seg) in route_segments.iter().zip(req_segments.clone()) {
172        if route_seg.dynamic_trail {
173            return true;
174        }
175
176        if !route_seg.dynamic && route_seg.value != req_seg {
177            return false;
178        }
179    }
180
181    true
182}
183
184fn queries_match(route: &Route, req: &Request<'_>) -> bool {
185    trace!(
186        route.query = route.uri.query().map(display),
187        route.query.color = route.uri.metadata.query_color.map(debug),
188        request.query = req.uri().query().map(display),
189    );
190
191    if matches!(route.uri.metadata.query_color, None | Some(Color::Wild)) {
192        return true;
193    }
194
195    let route_query_fields = route.uri.metadata.static_query_fields.iter();
196    for (key, val) in route_query_fields {
197        if let Some(query) = req.uri().query() {
198            if !query.segments().any(|req_seg| req_seg == (key, val)) {
199                debug!(key, val, request.query = %query, "missing static query");
200                return false;
201            }
202        } else {
203            debug!(key, val, "missing static query (queryless request)");
204            return false;
205        }
206    }
207
208    true
209}
210
211fn formats_match(route: &Route, req: &Request<'_>) -> bool {
212    trace!(
213        route.format = route.format.as_ref().map(display),
214        request.format = req.format().map(display),
215    );
216
217    let route_format = match route.format {
218        Some(ref format) => format,
219        None => return true,
220    };
221
222    match route.method.and_then(|m| m.allows_request_body()) {
223        Some(true) => match req.format() {
224            Some(f) if f.specificity() == 2 => route_format.collides_with(f),
225            _ => false,
226        },
227        _ => match req.format() {
228            Some(f) => route_format.collides_with(f),
229            None => true,
230        },
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use crate::http::{Accept, ContentType, MediaType, Method, Method::*};
237    use crate::local::blocking::Client;
238    use crate::route::{dummy_handler, Route};
239
240    fn req_matches_route(a: &'static str, b: &'static str) -> bool {
241        let client = Client::debug_with(vec![]).expect("client");
242        let route = Route::ranked(0, Get, b, dummy_handler);
243        route.matches(&client.get(a))
244    }
245
246    #[test]
247    fn request_route_matching() {
248        assert!(req_matches_route("/a/b?a=b", "/a/b?<c>"));
249        assert!(req_matches_route("/a/b?a=b", "/<a>/b?<c>"));
250        assert!(req_matches_route("/a/b?a=b", "/<a>/<b>?<c>"));
251        assert!(req_matches_route("/a/b?a=b", "/a/<b>?<c>"));
252        assert!(req_matches_route("/?b=c", "/?<b>"));
253
254        assert!(req_matches_route("/a/b?a=b", "/a/b"));
255        assert!(req_matches_route("/a/b", "/a/b"));
256        assert!(req_matches_route("/a/b/c/d?", "/a/b/c/d"));
257        assert!(req_matches_route("/a/b/c/d?v=1&v=2", "/a/b/c/d"));
258
259        assert!(req_matches_route("/a/b", "/a/b?<c>"));
260        assert!(req_matches_route("/a/b", "/a/b?<c..>"));
261        assert!(req_matches_route("/a/b?c", "/a/b?c"));
262        assert!(req_matches_route("/a/b?c", "/a/b?<c>"));
263        assert!(req_matches_route("/a/b?c=foo&d=z", "/a/b?<c>"));
264        assert!(req_matches_route("/a/b?c=foo&d=z", "/a/b?<c..>"));
265        assert!(req_matches_route("/a/b?c=foo&d=z", "/a/b?c=foo&<c..>"));
266        assert!(req_matches_route("/a/b?c=foo&d=z", "/a/b?d=z&<c..>"));
267
268        assert!(req_matches_route("/", "/<foo>"));
269        assert!(req_matches_route("/a", "/<foo>"));
270        assert!(req_matches_route("/a", "/a"));
271        assert!(req_matches_route("/a/", "/a/"));
272
273        assert!(req_matches_route("//", "/"));
274        assert!(req_matches_route("/a///", "/a/"));
275        assert!(req_matches_route("/a/b", "/a/b"));
276
277        assert!(!req_matches_route("/a///", "/a"));
278        assert!(!req_matches_route("/a", "/a/"));
279        assert!(!req_matches_route("/a/", "/a"));
280        assert!(!req_matches_route("/a/b", "/a/b/"));
281
282        assert!(!req_matches_route("/a", "/<a>/"));
283        assert!(!req_matches_route("/a/", "/<a>"));
284        assert!(!req_matches_route("/a/b", "/<a>/b/"));
285        assert!(!req_matches_route("/a/b", "/<a>/<b>/"));
286
287        assert!(!req_matches_route("/a/b/c", "/a/b?<c>"));
288        assert!(!req_matches_route("/a?b=c", "/a/b?<c>"));
289        assert!(!req_matches_route("/?b=c", "/a/b?<c>"));
290        assert!(!req_matches_route("/?b=c", "/a?<c>"));
291
292        assert!(!req_matches_route("/a/", "/<a>/<b>/<c..>"));
293        assert!(!req_matches_route("/a/b", "/<a>/<b>/<c..>"));
294
295        assert!(!req_matches_route("/a/b?c=foo&d=z", "/a/b?a=b&<c..>"));
296        assert!(!req_matches_route("/a/b?c=foo&d=z", "/a/b?d=b&<c..>"));
297        assert!(!req_matches_route("/a/b", "/a/b?c"));
298        assert!(!req_matches_route("/a/b", "/a/b?foo"));
299        assert!(!req_matches_route("/a/b", "/a/b?foo&<rest..>"));
300        assert!(!req_matches_route("/a/b", "/a/b?<a>&b&<rest..>"));
301    }
302
303    fn req_matches_format<S1, S2>(m: Method, mt1: S1, mt2: S2) -> bool
304    where
305        S1: Into<Option<&'static str>>,
306        S2: Into<Option<&'static str>>,
307    {
308        let client = Client::debug_with(vec![]).expect("client");
309        let mut req = client.req(m, "/");
310        if let Some(mt_str) = mt1.into() {
311            if m.allows_request_body() == Some(true) {
312                req.replace_header(mt_str.parse::<ContentType>().unwrap());
313            } else {
314                req.replace_header(mt_str.parse::<Accept>().unwrap());
315            }
316        }
317
318        let mut route = Route::new(m, "/", dummy_handler);
319        if let Some(mt_str) = mt2.into() {
320            route.format = Some(mt_str.parse::<MediaType>().unwrap());
321        }
322
323        route.matches(&req)
324    }
325
326    #[test]
327    fn test_req_route_mt_collisions() {
328        assert!(req_matches_format(
329            Post,
330            "application/json",
331            "application/json"
332        ));
333        assert!(req_matches_format(
334            Post,
335            "application/json",
336            "application/*"
337        ));
338        assert!(req_matches_format(Post, "application/json", "*/json"));
339        assert!(req_matches_format(Post, "text/html", "*/*"));
340
341        assert!(req_matches_format(
342            Get,
343            "application/json",
344            "application/json"
345        ));
346        assert!(req_matches_format(Get, "text/html", "text/html"));
347        assert!(req_matches_format(Get, "text/html", "*/*"));
348        assert!(req_matches_format(Get, None, "*/*"));
349        assert!(req_matches_format(Get, None, "text/*"));
350        assert!(req_matches_format(Get, None, "text/html"));
351        assert!(req_matches_format(Get, None, "application/json"));
352
353        assert!(req_matches_format(Post, "text/html", None));
354        assert!(req_matches_format(Post, "application/json", None));
355        assert!(req_matches_format(Post, "x-custom/anything", None));
356        assert!(req_matches_format(Post, None, None));
357
358        assert!(req_matches_format(Get, "text/html", None));
359        assert!(req_matches_format(Get, "application/json", None));
360        assert!(req_matches_format(Get, "x-custom/anything", None));
361        assert!(req_matches_format(Get, None, None));
362        assert!(req_matches_format(Get, None, "text/html"));
363        assert!(req_matches_format(Get, None, "application/json"));
364
365        assert!(req_matches_format(
366            Get,
367            "text/html, text/plain",
368            "text/html"
369        ));
370        assert!(req_matches_format(
371            Get,
372            "text/html; q=0.5, text/xml",
373            "text/xml"
374        ));
375
376        assert!(!req_matches_format(Post, None, "text/html"));
377        assert!(!req_matches_format(Post, None, "text/*"));
378        assert!(!req_matches_format(Post, None, "*/text"));
379        assert!(!req_matches_format(Post, None, "*/*"));
380        assert!(!req_matches_format(Post, None, "text/html"));
381        assert!(!req_matches_format(Post, None, "application/json"));
382
383        assert!(!req_matches_format(Post, "application/json", "text/html"));
384        assert!(!req_matches_format(Post, "application/json", "text/*"));
385        assert!(!req_matches_format(Post, "application/json", "*/xml"));
386        assert!(!req_matches_format(Get, "application/json", "text/html"));
387        assert!(!req_matches_format(Get, "application/json", "text/*"));
388        assert!(!req_matches_format(Get, "application/json", "*/xml"));
389
390        assert!(!req_matches_format(Post, None, "text/html"));
391        assert!(!req_matches_format(Post, None, "application/json"));
392    }
393}