diff --git a/core/codegen/tests/ui-fail-nightly/responder-types.stderr b/core/codegen/tests/ui-fail-nightly/responder-types.stderr index 6885bc43..84995fe0 100644 --- a/core/codegen/tests/ui-fail-nightly/responder-types.stderr +++ b/core/codegen/tests/ui-fail-nightly/responder-types.stderr @@ -117,8 +117,8 @@ error[E0277]: the trait bound `usize: Responder<'_, '_>` is not satisfied as Responder<'r, 'static>> as Responder<'r, 'static>> and $N others -note: required by a bound in `route::handler::, Status, rocket::Data<'o>>>::from` +note: required by a bound in `route::handler::, Status, (rocket::Data<'o>, Status)>>::from` --> $WORKSPACE/core/lib/src/route/handler.rs | | pub fn from>(req: &'r Request<'_>, responder: R) -> Outcome<'r> { - | ^^^^^^^^^^^^^^^^^ required by this bound in `route::handler::, Status, Data<'o>>>::from` + | ^^^^^^^^^^^^^^^^^ required by this bound in `route::handler::, Status, (Data<'o>, Status)>>::from` diff --git a/core/codegen/tests/ui-fail-stable/responder-types.stderr b/core/codegen/tests/ui-fail-stable/responder-types.stderr index 4f14b511..4dafb9f5 100644 --- a/core/codegen/tests/ui-fail-stable/responder-types.stderr +++ b/core/codegen/tests/ui-fail-stable/responder-types.stderr @@ -117,8 +117,8 @@ error[E0277]: the trait bound `usize: Responder<'_, '_>` is not satisfied as Responder<'r, 'static>> as Responder<'r, 'static>> and $N others -note: required by a bound in `route::handler::, Status, rocket::Data<'o>>>::from` +note: required by a bound in `route::handler::, Status, (rocket::Data<'o>, Status)>>::from` --> $WORKSPACE/core/lib/src/route/handler.rs | | pub fn from>(req: &'r Request<'_>, responder: R) -> Outcome<'r> { - | ^^^^^^^^^^^^^^^^^ required by this bound in `route::handler::, Status, Data<'o>>>::from` + | ^^^^^^^^^^^^^^^^^ required by this bound in `route::handler::, Status, (Data<'o>, Status)>>::from` diff --git a/core/http/src/tls/mtls.rs b/core/http/src/tls/mtls.rs index 65246d4d..f8b67dac 100644 --- a/core/http/src/tls/mtls.rs +++ b/core/http/src/tls/mtls.rs @@ -64,7 +64,8 @@ pub type Result = std::result::Result; /// configured `ca_certs` and with respect to SNI, if any. See [module level /// docs](crate::mtls) for configuration details. /// -/// If the client does not present certificates, the guard _forwards_. +/// If the client does not present certificates, the guard _forwards_ with a +/// status of 401 Unauthorized. /// /// If the certificate chain fails to validate or verify, the guard _fails_ with /// the respective [`Error`]. @@ -81,6 +82,7 @@ pub type Result = std::result::Result; /// use rocket::mtls::{self, bigint::BigUint, Certificate}; /// use rocket::request::{Request, FromRequest, Outcome}; /// use rocket::outcome::try_outcome; +/// use rocket::http::Status; /// /// // The serial number for the certificate issued to the admin. /// const ADMIN_SERIAL: &str = "65828378108300243895479600452308786010218223563"; @@ -97,7 +99,7 @@ pub type Result = std::result::Result; /// if let Some(true) = cert.has_serial(ADMIN_SERIAL) { /// Outcome::Success(CertifiedAdmin(cert)) /// } else { -/// Outcome::Forward(()) +/// Outcome::Forward(Status::Unauthorized) /// } /// } /// } diff --git a/core/lib/src/data/from_data.rs b/core/lib/src/data/from_data.rs index ac724609..2bc1aa47 100644 --- a/core/lib/src/data/from_data.rs +++ b/core/lib/src/data/from_data.rs @@ -22,10 +22,10 @@ impl<'r, S, E> IntoOutcome, Status)> for Result } #[inline] - fn or_forward(self, (data, error): (Data<'r>, Status)) -> Outcome<'r, S, E> { + fn or_forward(self, (data, status): (Data<'r>, Status)) -> Outcome<'r, S, E> { match self { Ok(val) => Success(val), - Err(_) => Forward((data, error)) + Err(_) => Forward((data, status)) } } } diff --git a/core/lib/src/request/from_request.rs b/core/lib/src/request/from_request.rs index e50a4742..33aa5ef8 100644 --- a/core/lib/src/request/from_request.rs +++ b/core/lib/src/request/from_request.rs @@ -142,7 +142,7 @@ impl IntoOutcome for Result { /// Extracts the [`Route`] from the request if one is available. When used /// as a request guard in a route handler, this will always succeed. Outside /// of a route handler, a route may not be available, and the request is -/// forwarded with a 500 status. +/// forwarded with a 500 Internal Server Error status. /// /// For more information on when an `&Route` is available, see /// [`Request::route()`]. @@ -165,19 +165,19 @@ impl IntoOutcome for Result { /// /// Extracts the [`ContentType`] from the incoming request via /// [`Request::content_type()`]. If the request didn't specify a -/// Content-Type, the request is forwarded. +/// Content-Type, the request is forwarded with a 404 Not Found status. /// /// * **IpAddr** /// /// Extracts the client ip address of the incoming request as an [`IpAddr`] /// via [`Request::client_ip()`]. If the client's IP address is not known, -/// the request is forwarded. +/// the request is forwarded with a 404 Not Found status. /// /// * **SocketAddr** /// /// Extracts the remote address of the incoming request as a [`SocketAddr`] /// via [`Request::remote()`]. If the remote address is not known, the -/// request is forwarded. +/// request is forwarded with a 404 Not Found status. /// /// * **Option<T>** _where_ **T: FromRequest** /// @@ -193,7 +193,7 @@ impl IntoOutcome for Result { /// `FromRequest` implementation. If derivation is a `Success`, the value is /// returned in `Ok`. If the derivation is a `Failure`, the error value is /// returned in `Err`. If the derivation is a `Forward`, the request is -/// forwarded. +/// forwarded with the same status code as the original forward. /// /// [`Config`]: crate::config::Config /// @@ -503,7 +503,7 @@ impl<'r, T: FromRequest<'r>> FromRequest<'r> for Result { match T::from_request(request).await { Success(val) => Success(Ok(val)), Failure((_, e)) => Success(Err(e)), - Forward(_) => Forward(Status::NotFound), + Forward(status) => Forward(status), } } } @@ -519,3 +519,12 @@ impl<'r, T: FromRequest<'r>> FromRequest<'r> for Option { } } } + +#[crate::async_trait] +impl<'r, T: FromRequest<'r>> FromRequest<'r> for Outcome { + type Error = std::convert::Infallible; + + async fn from_request(request: &'r Request<'_>) -> Outcome { + Success(T::from_request(request).await) + } +} diff --git a/core/lib/src/route/handler.rs b/core/lib/src/route/handler.rs index 739639d0..9fee0d67 100644 --- a/core/lib/src/route/handler.rs +++ b/core/lib/src/route/handler.rs @@ -221,8 +221,8 @@ impl<'r, 'o: 'r> Outcome<'o> { /// Return the `Outcome` of response to `req` from `responder`. /// /// If the responder returns `Ok`, an outcome of `Success` is returned with - /// the response. If the responder returns `Err`, an outcome of `Forward` is - /// returned. + /// the response. If the responder returns `Err`, an outcome of `Forward` + /// with a status of `404 Not Found` is returned. /// /// # Example /// diff --git a/core/lib/src/state.rs b/core/lib/src/state.rs index b323f77d..577e873b 100644 --- a/core/lib/src/state.rs +++ b/core/lib/src/state.rs @@ -80,7 +80,7 @@ use crate::http::Status; /// // Or alternatively, using `Rocket::state()`: /// let outcome = request.rocket().state::() /// .map(|my_config| Item(&my_config.user_val)) -/// .or_forward(Status::NotFound); +/// .or_forward(Status::InternalServerError); /// /// outcome /// } diff --git a/core/lib/tests/forward-includes-error-1560.rs b/core/lib/tests/forward-includes-error-1560.rs deleted file mode 100644 index 73cb145b..00000000 --- a/core/lib/tests/forward-includes-error-1560.rs +++ /dev/null @@ -1,79 +0,0 @@ -#[macro_use] extern crate rocket; - -use rocket::http::Status; -use rocket::request::{self, Request, FromRequest}; - -pub struct Authenticated; - -#[rocket::async_trait] -impl<'r> FromRequest<'r> for Authenticated { - type Error = std::convert::Infallible; - - async fn from_request(request: &'r Request<'_>) -> request::Outcome { - if request.headers().contains("Authenticated") { - request::Outcome::Success(Authenticated) - } else { - request::Outcome::Forward(Status::Unauthorized) - } - } -} - -#[get("/one")] -pub async fn get_protected_one(_user: Authenticated) -> &'static str { - "Protected" -} - -#[get("/one", rank = 2)] -pub async fn get_public_one() -> &'static str { - "Public" -} - -#[get("/two")] -pub async fn get_protected_two(_user: Authenticated) -> &'static str { - "Protected" -} - -mod tests { - use super::*; - use rocket::routes; - use rocket::local::blocking::Client; - use rocket::http::{Header, Status}; - - #[test] - fn one_protected_returned_for_authenticated() { - let rocket = rocket::build().mount("/", - routes![get_protected_one, get_public_one, get_protected_two]); - - let client = Client::debug(rocket).unwrap(); - let req = client.get("/one").header(Header::new("Authenticated", "true")); - let response = req.dispatch(); - - assert_eq!(response.status(), Status::Ok); - assert_eq!(response.into_string(), Some("Protected".into())); - } - - #[test] - fn one_public_returned_for_unauthenticated() { - let rocket = rocket::build().mount("/", - routes![get_protected_one, get_public_one, get_protected_two]); - - let client = Client::debug(rocket).unwrap(); - let req = client.get("/one"); - let response = req.dispatch(); - - assert_eq!(response.status(), Status::Ok); - assert_eq!(response.into_string(), Some("Public".into())); - } - - #[test] - fn two_unauthorized_returned_for_unauthenticated() { - let rocket = rocket::build().mount("/", - routes![get_protected_one, get_public_one, get_protected_two]); - - let client = Client::debug(rocket).unwrap(); - let req = client.get("/two"); - let response = req.dispatch(); - - assert_eq!(response.status(), Status::Unauthorized); - } -} diff --git a/core/lib/tests/forward-includes-status-1560.rs b/core/lib/tests/forward-includes-status-1560.rs new file mode 100644 index 00000000..b325672a --- /dev/null +++ b/core/lib/tests/forward-includes-status-1560.rs @@ -0,0 +1,108 @@ +#[macro_use] extern crate rocket; + +use rocket::http::Status; +use rocket::request::{self, Request, FromRequest}; + +struct Authenticated; + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for Authenticated { + type Error = std::convert::Infallible; + + async fn from_request(request: &'r Request<'_>) -> request::Outcome { + if request.headers().contains("Authenticated") { + request::Outcome::Success(Authenticated) + } else { + request::Outcome::Forward(Status::Unauthorized) + } + } +} + +struct TeapotForward; + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for TeapotForward { + type Error = std::convert::Infallible; + + async fn from_request(_: &'r Request<'_>) -> request::Outcome { + request::Outcome::Forward(Status::ImATeapot) + } +} + +#[get("/auth")] +fn auth(_name: Authenticated) -> &'static str { + "Protected" +} + +#[get("/auth", rank = 2)] +fn public() -> &'static str { + "Public" +} + +#[get("/auth", rank = 3)] +fn teapot(_teapot: TeapotForward) -> &'static str { + "Protected" +} + +#[get("/need-auth")] +fn auth_needed(_auth: Authenticated) -> &'static str { + "Have Auth" +} + +#[catch(401)] +fn catcher() -> &'static str { + "Custom Catcher" +} + +mod tests { + use super::*; + use rocket::routes; + use rocket::local::blocking::Client; + use rocket::http::{Header, Status}; + + #[test] + fn authorized_forwards() { + let client = Client::debug_with(routes![auth, public, auth_needed]).unwrap(); + + let response = client.get("/auth") + .header(Header::new("Authenticated", "true")) + .dispatch(); + + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.into_string().unwrap(), "Protected"); + + let response = client.get("/auth").dispatch(); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.into_string().unwrap(), "Public"); + + let response = client.get("/need-auth") + .header(Header::new("Authenticated", "true")) + .dispatch(); + + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.into_string().unwrap(), "Have Auth"); + + let response = client.get("/need-auth").dispatch(); + assert_eq!(response.status(), Status::Unauthorized); + assert!(response.into_string().unwrap().contains("Rocket")); + } + + #[test] + fn unauthorized_custom_catcher() { + let rocket = rocket::build() + .mount("/", routes![auth_needed]) + .register("/", catchers![catcher]); + + let client = Client::debug(rocket).unwrap(); + let response = client.get("/need-auth").dispatch(); + assert_eq!(response.status(), Status::Unauthorized); + assert_eq!(response.into_string().unwrap(), "Custom Catcher"); + } + + #[test] + fn use_last_forward() { + let client = Client::debug_with(routes![auth, teapot]).unwrap(); + let response = client.get("/auth").dispatch(); + assert_eq!(response.status(), Status::ImATeapot); + } +} diff --git a/examples/cookies/src/session.rs b/examples/cookies/src/session.rs index de74831b..c4309a42 100644 --- a/examples/cookies/src/session.rs +++ b/examples/cookies/src/session.rs @@ -24,7 +24,7 @@ impl<'r> FromRequest<'r> for User { .get_private("user_id") .and_then(|cookie| cookie.value().parse().ok()) .map(User) - .or_forward(Status::NotFound) + .or_forward(Status::Unauthorized) } } diff --git a/site/guide/4-requests.md b/site/guide/4-requests.md index 45670455..4fe8fee5 100644 --- a/site/guide/4-requests.md +++ b/site/guide/4-requests.md @@ -206,9 +206,10 @@ fn hello(name: &str, age: u8, cool: bool) { /* ... */ } What if `cool` isn't a `bool`? Or, what if `age` isn't a `u8`? When a parameter type mismatch occurs, Rocket _forwards_ the request to the next matching route, -if there is any. This continues until a route doesn't forward the request or -there are no remaining routes to try. When there are no remaining routes, a -customizable **404 error** is returned. +if there is any. This continues until a route succeeds or fails, or there are no +other matching routes to try. When there are no remaining routes, the [error +catcher](#error-catchers) associated with the status set by the last forwarding +guard is called. Routes are attempted in increasing _rank_ order. Rocket chooses a default ranking from -12 to -1, detailed in the next section, but a route's rank can also @@ -436,13 +437,14 @@ We start with two request guards: The `FromRequest` implementation for `User` checks that a cookie identifies a user and returns a `User` value if so. If no user can be authenticated, - the guard forwards. + the guard forwards with a 401 Unauthorized status. * `AdminUser`: A user authenticated as an administrator. The `FromRequest` implementation for `AdminUser` checks that a cookie identifies an _administrative_ user and returns an `AdminUser` value if so. - If no user can be authenticated, the guard forwards. + If no user can be authenticated, the guard forwards with a 401 Unauthorized + status. We now use these two guards in combination with forwarding to implement the following three routes, each leading to an administrative control panel at diff --git a/site/guide/6-state.md b/site/guide/6-state.md index e884788a..0eb6d65a 100644 --- a/site/guide/6-state.md +++ b/site/guide/6-state.md @@ -141,14 +141,13 @@ impl<'r> FromRequest<'r> for Item<'r> { // Or alternatively, using `Rocket::state()`: let outcome = request.rocket().state::() .map(|my_config| Item(&my_config.user_val)) - .or_forward(Status::NotFound); + .or_forward(Status::InternalServerError); outcome } } ``` - [`Request::guard()`]: @api/rocket/struct.Request.html#method.guard [`Rocket::state()`]: @api/rocket/struct.Rocket.html#method.state