Skip to content

Commit be52278

Browse files
committed
Fix rwf2#1224 by searching routes after failed match
1 parent 3bf9ef0 commit be52278

File tree

11 files changed

+103
-14
lines changed

11 files changed

+103
-14
lines changed

core/codegen/tests/route-format.rs

+6-3
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ fn test_formats() {
6161
assert_eq!(response.into_string().unwrap(), "plain");
6262

6363
let response = client.put("/").header(ContentType::HTML).dispatch();
64-
assert_eq!(response.status(), Status::NotFound);
64+
assert_eq!(response.status(), Status::MethodNotAllowed);
6565
}
6666

6767
// Test custom formats.
@@ -109,9 +109,12 @@ fn test_custom_formats() {
109109
let response = client.get("/").dispatch();
110110
assert_eq!(response.into_string().unwrap(), "get_foo");
111111

112+
let response = client.get("/").header(Accept::JPEG).dispatch();
113+
assert_eq!(response.status(), Status::NotAcceptable); // Route can't produce JPEG
114+
112115
let response = client.put("/").header(ContentType::HTML).dispatch();
113-
assert_eq!(response.status(), Status::NotFound);
116+
assert_eq!(response.status(), Status::UnsupportedMediaType); // Route expects "bar/baz"
114117

115118
let response = client.post("/").header(ContentType::HTML).dispatch();
116-
assert_eq!(response.status(), Status::NotFound);
119+
assert_eq!(response.status(), Status::UnsupportedMediaType); // Route expects "foo"
117120
}

core/codegen/tests/route.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ fn test_full_route() {
105105
assert_eq!(response.status(), Status::NotFound);
106106

107107
let response = client.post(format!("/1{}", uri)).body(simple).dispatch();
108-
assert_eq!(response.status(), Status::NotFound);
108+
assert_eq!(response.status(), Status::UnsupportedMediaType);
109109

110110
let response = client
111111
.post(format!("/1{}", uri))
@@ -117,7 +117,7 @@ fn test_full_route() {
117117
sky, name.percent_decode().unwrap(), "A A", "inside", path, simple, expected_uri));
118118

119119
let response = client.post(format!("/2{}", uri)).body(simple).dispatch();
120-
assert_eq!(response.status(), Status::NotFound);
120+
assert_eq!(response.status(), Status::UnsupportedMediaType);
121121

122122
let response = client
123123
.post(format!("/2{}", uri))

core/lib/src/lifecycle.rs

+19
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,25 @@ impl Rocket<Orbit> {
112112
Outcome::Forward((_, status)) => self.dispatch_error(status, request).await,
113113
}
114114
}
115+
Outcome::Forward((_, status)) if status == Status::NotFound => {
116+
// We failed to find a route which matches on path, query AND formats.
117+
// Before we return the error to the user, we'll check for "near misses".
118+
119+
// This code path primarily exists to assist clients in debugging their requests.
120+
let mut status = status;
121+
if self.router.matches_except_formats(request) {
122+
// Tailor the error code to the interpretation of the request in question.
123+
if request.method().allows_request_body() == Some(true) {
124+
status = Status::UnsupportedMediaType;
125+
} else if request.headers().contains("Accept") {
126+
status = Status::NotAcceptable;
127+
}
128+
} else if self.router.matches_except_method(request) {
129+
// Found a more suitable error code for paths implemented on different methods.
130+
status = Status::MethodNotAllowed;
131+
}
132+
self.dispatch_error(status, request).await
133+
}
115134
Outcome::Forward((_, status)) => self.dispatch_error(status, request).await,
116135
Outcome::Error(status) => self.dispatch_error(status, request).await,
117136
};

core/lib/src/router/matcher.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ fn methods_match(route: &Route, req: &Request<'_>) -> bool {
144144
route.method.map_or(true, |method| method == req.method())
145145
}
146146

147-
fn paths_match(route: &Route, req: &Request<'_>) -> bool {
147+
pub(crate) fn paths_match(route: &Route, req: &Request<'_>) -> bool {
148148
trace!(route.uri = %route.uri, request.uri = %req.uri());
149149
let route_segments = &route.uri.metadata.uri_segments;
150150
let req_segments = req.uri().path().segments();
@@ -174,7 +174,7 @@ fn paths_match(route: &Route, req: &Request<'_>) -> bool {
174174
true
175175
}
176176

177-
fn queries_match(route: &Route, req: &Request<'_>) -> bool {
177+
pub(crate) fn queries_match(route: &Route, req: &Request<'_>) -> bool {
178178
trace!(
179179
route.query = route.uri.query().map(display),
180180
route.query.color = route.uri.metadata.query_color.map(debug),
@@ -201,7 +201,7 @@ fn queries_match(route: &Route, req: &Request<'_>) -> bool {
201201
true
202202
}
203203

204-
fn formats_match(route: &Route, req: &Request<'_>) -> bool {
204+
pub(crate) fn formats_match(route: &Route, req: &Request<'_>) -> bool {
205205
trace!(
206206
route.format = route.format.as_ref().map(display),
207207
request.format = req.format().map(display),

core/lib/src/router/router.rs

+53
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use crate::http::{Method, Status};
66
use crate::{Route, Catcher};
77
use crate::router::Collide;
88

9+
use super::matcher::{paths_match, queries_match, formats_match};
10+
911
#[derive(Debug)]
1012
pub(crate) struct Router<T>(T);
1113

@@ -104,6 +106,38 @@ impl Router<Finalized> {
104106
.filter(move |r| r.matches(req))
105107
}
106108

109+
pub(crate) fn matches_except_formats<'r, 'a: 'r>(
110+
&'a self,
111+
req: &'r Request<'r>
112+
) -> bool {
113+
self.route_map.get(&req.method())
114+
.into_iter()
115+
.flatten()
116+
.any(|&route|
117+
paths_match(&self.routes[route], req)
118+
&&
119+
queries_match(&self.routes[route], req)
120+
&&
121+
!formats_match(&self.routes[route], req))
122+
}
123+
124+
const ALL_METHODS: &'static [Method] = &[
125+
Method::Get, Method::Put, Method::Post, Method::Delete, Method::Options,
126+
Method::Head, Method::Trace, Method::Connect, Method::Patch,
127+
];
128+
129+
pub(crate) fn matches_except_method<'r, 'a: 'r>(
130+
&'a self,
131+
req: &'r Request<'r>
132+
) -> bool {
133+
Self::ALL_METHODS
134+
.iter()
135+
.filter(|method| *method != &req.method())
136+
.filter_map(|method| self.route_map.get(method))
137+
.flatten()
138+
.any(|route| paths_match(&self.routes[*route], req))
139+
}
140+
107141
// For many catchers, using aho-corasick or similar should be much faster.
108142
#[track_caller]
109143
pub fn catch<'r>(&self, status: Status, req: &'r Request<'r>) -> Option<&Catcher> {
@@ -396,6 +430,25 @@ mod test {
396430
assert!(route(&router, Get, "/prefi/").is_none());
397431
}
398432

433+
fn has_mismatched_method<'a>(
434+
router: &'a Router<Finalized>,
435+
method: Method, uri: &'a str
436+
) -> bool {
437+
let client = Client::debug_with(vec![]).expect("client");
438+
let request = client.req(method, Origin::parse(uri).unwrap());
439+
router.matches_except_method(&request)
440+
}
441+
442+
#[test]
443+
fn test_bad_method_routing() {
444+
let router = router_with_routes(&["/hello"]);
445+
assert!(route(&router, Put, "/hello").is_none());
446+
assert!(has_mismatched_method(&router, Put, "/hello"));
447+
assert!(has_mismatched_method(&router, Post, "/hello"));
448+
449+
assert!(! has_mismatched_method(&router, Get, "/hello"));
450+
}
451+
399452
/// Asserts that `$to` routes to `$want` given `$routes` are present.
400453
macro_rules! assert_ranked_match {
401454
($routes:expr, $to:expr => $want:expr) => ({

core/lib/tests/form_method-issue-45.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,6 @@ mod tests {
7070
.body("_method=patch&form_data=Form+data")
7171
.dispatch();
7272

73-
assert_eq!(response.status(), Status::NotFound);
73+
assert_eq!(response.status(), Status::MethodNotAllowed);
7474
}
7575
}

core/lib/tests/precise-content-type-matching.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ mod tests {
4848
let body: Option<&'static str> = $body;
4949
match body {
5050
Some(string) => assert_eq!(body_str, Some(string.to_string())),
51-
None => assert_eq!(status, Status::NotFound)
51+
None => assert_eq!(status, Status::UnsupportedMediaType)
5252
}
5353
)
5454
}

docs/guide/05-requests.md

+7
Original file line numberDiff line numberDiff line change
@@ -2074,6 +2074,13 @@ fn main() {
20742074
}
20752075
```
20762076

2077+
Besides `404 Not Found` for unknown URIs, Rocket may also produce
2078+
`405 Method Not Allowed` if a request matches a URI but not a declared method
2079+
for that URI. For routes declaring formats, Rocket will produce
2080+
`406 Not Acceptable` status for a client request _accepting_ a format which
2081+
isn't declared by the matching routes, or `415 Unsupported Media Type` in case
2082+
the _payload_ of a `PUT` or `POST` is not allowed by the route.
2083+
20772084
### Scoping
20782085

20792086
The first argument to `register()` is a path to scope the catcher under called

examples/error-handling/src/tests.rs

+9
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,15 @@ fn test_hello_invalid_age() {
6464
}
6565
}
6666

67+
#[test]
68+
fn test_method_not_allowed() {
69+
let client = Client::tracked(super::rocket()).unwrap();
70+
let (name, age) = ("Pat", 86);
71+
let request = client.post(format!("/hello/{}/{}", name, age)).body("body");
72+
let response = request.dispatch();
73+
assert_eq!(response.status(), Status::MethodNotAllowed);
74+
}
75+
6776
#[test]
6877
fn test_hello_sergio() {
6978
let client = Client::tracked(super::rocket()).unwrap();

examples/responders/src/tests.rs

+1-2
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,7 @@ fn test_xml() {
126126
assert_eq!(r.into_string().unwrap(), r#"{ "payload": "I'm here" }"#);
127127

128128
let r = client.get(uri!(super::xml)).header(Accept::CSV).dispatch();
129-
assert_eq!(r.status(), Status::NotFound);
130-
assert!(r.into_string().unwrap().contains("not supported"));
129+
assert_eq!(r.status(), Status::NotAcceptable);
131130

132131
let r = client.get("/content/i/dont/exist").header(Accept::HTML).dispatch();
133132
assert_eq!(r.content_type().unwrap(), ContentType::HTML);

examples/templating/src/tests.rs

+1-2
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ fn test_root(kind: &str) {
2222
let expected = Template::show(client.rocket(), format!("{}/error/404", kind), &context);
2323

2424
let response = client.req(*method, format!("/{}", kind)).dispatch();
25-
assert_eq!(response.status(), Status::NotFound);
26-
assert_eq!(response.into_string(), expected);
25+
assert_eq!(response.status(), Status::MethodNotAllowed);
2726
}
2827
}
2928

0 commit comments

Comments
 (0)