Allow status customization in 'Forward' outcomes.

Prior to this commit, all forward outcomes resulted in a 404. This
commit changes request and data guards so that they are able to provide
a `Status` on `Forward` outcomes. The router uses this status, if the
final outcome is to forward, to identify the catcher to invoke.

The net effect is that guards can now customize the status code of a
forward and thus the error catcher invoked if the final outcome of a
request is to forward.

Resolves #1560.
This commit is contained in:
Benedikt Weber 2023-04-11 11:54:05 -07:00 committed by Sergio Benitez
parent b61ac6eb18
commit 055ad107df
16 changed files with 159 additions and 68 deletions

View File

@ -4,8 +4,8 @@ use std::pin::Pin;
use rocket::data::{IoHandler, IoStream};
use rocket::futures::{self, StreamExt, SinkExt, future::BoxFuture, stream::SplitStream};
use rocket::response::{self, Responder, Response};
use rocket::request::{FromRequest, Outcome};
use rocket::request::Request;
use rocket::request::{FromRequest, Request, Outcome};
use rocket::http::Status;
use crate::{Config, Message};
use crate::stream::DuplexStream;
@ -203,7 +203,7 @@ impl<'r> FromRequest<'r> for WebSocket {
let key = headers.get_one("Sec-WebSocket-Key").map(|k| derive_accept_key(k.as_bytes()));
match key {
Some(key) if is_upgrade && is_ws && is_13 => Outcome::Success(WebSocket::new(key)),
Some(_) | None => Outcome::Forward(())
Some(_) | None => Outcome::Forward(Status::NotFound)
}
}
}

View File

@ -38,7 +38,7 @@ fn query_decls(route: &Route) -> Option<TokenStream> {
}
define_spanned_export!(Span::call_site() =>
__req, __data, _log, _form, Outcome, _Ok, _Err, _Some, _None
__req, __data, _log, _form, Outcome, _Ok, _Err, _Some, _None, Status
);
// Record all of the static parameters for later filtering.
@ -104,7 +104,7 @@ fn query_decls(route: &Route) -> Option<TokenStream> {
if !__e.is_empty() {
#_log::warn_!("Query string failed to match route declaration.");
for _err in __e { #_log::warn_!("{}", _err); }
return #Outcome::Forward(#__data);
return #Outcome::Forward((#__data, #Status::NotFound));
}
(#(#ident.unwrap()),*)
@ -121,9 +121,9 @@ fn request_guard_decl(guard: &Guard) -> TokenStream {
quote_spanned! { ty.span() =>
let #ident: #ty = match <#ty as #FromRequest>::from_request(#__req).await {
#Outcome::Success(__v) => __v,
#Outcome::Forward(_) => {
#Outcome::Forward(__e) => {
#_log::warn_!("Request guard `{}` is forwarding.", stringify!(#ty));
return #Outcome::Forward(#__data);
return #Outcome::Forward((#__data, __e));
},
#Outcome::Failure((__c, __e)) => {
#_log::warn_!("Request guard `{}` failed: {:?}.", stringify!(#ty), __e);
@ -137,7 +137,7 @@ fn param_guard_decl(guard: &Guard) -> TokenStream {
let (i, name, ty) = (guard.index, &guard.name, &guard.ty);
define_spanned_export!(ty.span() =>
__req, __data, _log, _None, _Some, _Ok, _Err,
Outcome, FromSegments, FromParam
Outcome, FromSegments, FromParam, Status
);
// Returned when a dynamic parameter fails to parse.
@ -145,7 +145,7 @@ fn param_guard_decl(guard: &Guard) -> TokenStream {
#_log::warn_!("Parameter guard `{}: {}` is forwarding: {:?}.",
#name, stringify!(#ty), __error);
#Outcome::Forward(#__data)
#Outcome::Forward((#__data, #Status::NotFound))
});
// All dynamic parameters should be found if this function is being called;
@ -161,7 +161,7 @@ fn param_guard_decl(guard: &Guard) -> TokenStream {
#_log::error_!("Internal invariant broken: dyn param {} not found.", #i);
#_log::error_!("Please report this to the Rocket issue tracker.");
#_log::error_!("https://github.com/SergioBenitez/Rocket/issues");
return #Outcome::Forward(#__data);
return #Outcome::Forward((#__data, #Status::InternalServerError));
}
}
},
@ -184,9 +184,9 @@ fn data_guard_decl(guard: &Guard) -> TokenStream {
quote_spanned! { ty.span() =>
let #ident: #ty = match <#ty as #FromData>::from_data(#__req, #__data).await {
#Outcome::Success(__d) => __d,
#Outcome::Forward(__d) => {
#Outcome::Forward((__d, __e)) => {
#_log::warn_!("Data guard `{}` is forwarding.", stringify!(#ty));
return #Outcome::Forward(__d);
return #Outcome::Forward((__d, __e));
}
#Outcome::Failure((__c, __e)) => {
#_log::warn_!("Data guard `{}` failed: {:?}.", stringify!(#ty), __e);

View File

@ -101,8 +101,8 @@ pub use self::cookie::{Cookie, SameSite, Iter};
/// # #[macro_use] extern crate rocket;
/// # #[cfg(feature = "secrets")] {
/// use rocket::http::Status;
/// use rocket::outcome::IntoOutcome;
/// use rocket::request::{self, Request, FromRequest};
/// use rocket::outcome::IntoOutcome;
///
/// // In practice, we'd probably fetch the user from the database.
/// struct User(usize);
@ -116,7 +116,7 @@ pub use self::cookie::{Cookie, SameSite, Iter};
/// .get_private("user_id")
/// .and_then(|c| c.value().parse().ok())
/// .map(|id| User(id))
/// .or_forward(())
/// .or_forward(Status::Unauthorized)
/// }
/// }
/// # }

View File

@ -102,6 +102,7 @@ impl<'r> Data<'r> {
/// ```rust
/// use rocket::request::{self, Request, FromRequest};
/// use rocket::data::{Data, FromData, Outcome};
/// use rocket::http::Status;
/// # struct MyType;
/// # type MyError = String;
///
@ -111,7 +112,7 @@ impl<'r> Data<'r> {
///
/// async fn from_data(r: &'r Request<'_>, mut data: Data<'r>) -> Outcome<'r, Self> {
/// if data.peek(2).await != b"hi" {
/// return Outcome::Forward(data)
/// return Outcome::Forward((data, Status::NotFound))
/// }
///
/// /* .. */

View File

@ -7,11 +7,11 @@ use crate::outcome::{self, IntoOutcome, try_outcome, Outcome::*};
///
/// [`FromData`]: crate::data::FromData
pub type Outcome<'r, T, E = <T as FromData<'r>>::Error>
= outcome::Outcome<T, (Status, E), Data<'r>>;
= outcome::Outcome<T, (Status, E), (Data<'r>, Status)>;
impl<'r, S, E> IntoOutcome<S, (Status, E), Data<'r>> for Result<S, E> {
impl<'r, S, E> IntoOutcome<S, (Status, E), (Data<'r>, Status)> for Result<S, E> {
type Failure = Status;
type Forward = Data<'r>;
type Forward = (Data<'r>, Status);
#[inline]
fn into_outcome(self, status: Status) -> Outcome<'r, S, E> {
@ -22,10 +22,10 @@ impl<'r, S, E> IntoOutcome<S, (Status, E), Data<'r>> for Result<S, E> {
}
#[inline]
fn or_forward(self, data: Data<'r>) -> Outcome<'r, S, E> {
fn or_forward(self, (data, error): (Data<'r>, Status)) -> Outcome<'r, S, E> {
match self {
Ok(val) => Success(val),
Err(_) => Forward(data)
Err(_) => Forward((data, error))
}
}
}
@ -130,7 +130,7 @@ impl<'r, S, E> IntoOutcome<S, (Status, E), Data<'r>> for Result<S, E> {
/// // Ensure the content type is correct before opening the data.
/// let person_ct = ContentType::new("application", "x-person");
/// if req.content_type() != Some(&person_ct) {
/// return Forward(data);
/// return Forward((data, Status::NotFound));
/// }
///
/// // Use a configured limit with name 'person' or fallback to default.

View File

@ -4,7 +4,7 @@ use either::Either;
use crate::request::{Request, local_cache_once};
use crate::data::{Data, Limits, Outcome};
use crate::form::{SharedStack, prelude::*};
use crate::http::RawStr;
use crate::http::{RawStr, Status};
type Result<'r, T> = std::result::Result<T, Error<'r>>;
@ -35,7 +35,7 @@ impl<'r, 'i> Parser<'r, 'i> {
let parser = match req.content_type() {
Some(c) if c.is_form() => Self::from_form(req, data).await,
Some(c) if c.is_form_data() => Self::from_multipart(req, data).await,
_ => return Outcome::Forward(data),
_ => return Outcome::Forward((data, Status::NotFound)),
};
match parser {

View File

@ -18,8 +18,8 @@ impl<'r> FromRequest<'r> for Certificate<'r> {
type Error = Error;
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let certs = try_outcome!(req.connection.client_certificates.as_ref().or_forward(()));
let data = try_outcome!(certs.chain_data().or_forward(()));
let certs = req.connection.client_certificates.as_ref().or_forward(Status::Unauthorized);
let data = try_outcome!(try_outcome!(certs).chain_data().or_forward(Status::Unauthorized));
Certificate::parse(data).into_outcome(Status::Unauthorized)
}
}

View File

@ -9,11 +9,11 @@ use crate::http::{Status, ContentType, Accept, Method, CookieJar};
use crate::http::uri::{Host, Origin};
/// Type alias for the `Outcome` of a `FromRequest` conversion.
pub type Outcome<S, E> = outcome::Outcome<S, (Status, E), ()>;
pub type Outcome<S, E> = outcome::Outcome<S, (Status, E), Status>;
impl<S, E> IntoOutcome<S, (Status, E), ()> for Result<S, E> {
impl<S, E> IntoOutcome<S, (Status, E), Status> for Result<S, E> {
type Failure = Status;
type Forward = ();
type Forward = Status;
#[inline]
fn into_outcome(self, status: Status) -> Outcome<S, E> {
@ -24,10 +24,10 @@ impl<S, E> IntoOutcome<S, (Status, E), ()> for Result<S, E> {
}
#[inline]
fn or_forward(self, _: ()) -> Outcome<S, E> {
fn or_forward(self, status: Status) -> Outcome<S, E> {
match self {
Ok(val) => Success(val),
Err(_) => Forward(())
Err(_) => Forward(status)
}
}
}
@ -102,16 +102,18 @@ impl<S, E> IntoOutcome<S, (Status, E), ()> for Result<S, E> {
/// * **Failure**(Status, E)
///
/// If the `Outcome` is [`Failure`], the request will fail with the given
/// status code and error. The designated error [`Catcher`](crate::Catcher) will be
/// used to respond to the request. Note that users can request types of
/// `Result<S, E>` and `Option<S>` to catch `Failure`s and retrieve the error
/// value.
/// status code and error. The designated error [`Catcher`](crate::Catcher)
/// will be used to respond to the request. Note that users can request types
/// of `Result<S, E>` and `Option<S>` to catch `Failure`s and retrieve the
/// error value.
///
/// * **Forward**
/// * **Forward**(Status)
///
/// If the `Outcome` is [`Forward`], the request will be forwarded to the next
/// matching route. Note that users can request an `Option<S>` to catch
/// `Forward`s.
/// matching route until either one succeds or there are no further matching
/// routes to attempt. In the latter case, the request will be sent to the
/// [`Catcher`](crate::Catcher) for the designated `Status`. Note that users
/// can request an `Option<S>` to catch `Forward`s.
///
/// # Provided Implementations
///
@ -137,10 +139,12 @@ impl<S, E> IntoOutcome<S, (Status, E), ()> for Result<S, E> {
///
/// * **&Route**
///
/// Extracts the [`Route`] from the request if one is available. If a route
/// is not available, the request is forwarded.
/// 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.
///
/// For information on when an `&Route` is available, see
/// For more information on when an `&Route` is available, see
/// [`Request::route()`].
///
/// * **&CookieJar**
@ -256,6 +260,7 @@ impl<S, E> IntoOutcome<S, (Status, E), ()> for Result<S, E> {
/// # #[cfg(feature = "secrets")] mod wrapper {
/// # use rocket::outcome::{IntoOutcome, try_outcome};
/// # use rocket::request::{self, Outcome, FromRequest, Request};
/// # use rocket::http::Status;
/// # struct User { id: String, is_admin: bool }
/// # struct Database;
/// # impl Database {
@ -283,7 +288,7 @@ impl<S, E> IntoOutcome<S, (Status, E), ()> for Result<S, E> {
/// .get_private("user_id")
/// .and_then(|cookie| cookie.value().parse().ok())
/// .and_then(|id| db.get_user(id).ok())
/// .or_forward(())
/// .or_forward(Status::Unauthorized)
/// }
/// }
///
@ -297,7 +302,7 @@ impl<S, E> IntoOutcome<S, (Status, E), ()> for Result<S, E> {
/// if user.is_admin {
/// Outcome::Success(Admin { user })
/// } else {
/// Outcome::Forward(())
/// Outcome::Forward(Status::Unauthorized)
/// }
/// }
/// }
@ -320,6 +325,7 @@ impl<S, E> IntoOutcome<S, (Status, E), ()> for Result<S, E> {
/// # #[cfg(feature = "secrets")] mod wrapper {
/// # use rocket::outcome::{IntoOutcome, try_outcome};
/// # use rocket::request::{self, Outcome, FromRequest, Request};
/// # use rocket::http::Status;
/// # struct User { id: String, is_admin: bool }
/// # struct Database;
/// # impl Database {
@ -352,7 +358,7 @@ impl<S, E> IntoOutcome<S, (Status, E), ()> for Result<S, E> {
/// .and_then(|id| db.get_user(id).ok())
/// }).await;
///
/// user_result.as_ref().or_forward(())
/// user_result.as_ref().or_forward(Status::Unauthorized)
/// }
/// }
///
@ -365,7 +371,7 @@ impl<S, E> IntoOutcome<S, (Status, E), ()> for Result<S, E> {
/// if user.is_admin {
/// Outcome::Success(Admin { user })
/// } else {
/// Outcome::Forward(())
/// Outcome::Forward(Status::Unauthorized)
/// }
/// }
/// }
@ -415,7 +421,7 @@ impl<'r> FromRequest<'r> for &'r Host<'r> {
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
match request.host() {
Some(host) => Success(host),
None => Forward(())
None => Forward(Status::NotFound)
}
}
}
@ -427,7 +433,7 @@ impl<'r> FromRequest<'r> for &'r Route {
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
match request.route() {
Some(route) => Success(route),
None => Forward(())
None => Forward(Status::InternalServerError)
}
}
}
@ -448,7 +454,7 @@ impl<'r> FromRequest<'r> for &'r Accept {
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
match request.accept() {
Some(accept) => Success(accept),
None => Forward(())
None => Forward(Status::NotFound)
}
}
}
@ -460,7 +466,7 @@ impl<'r> FromRequest<'r> for &'r ContentType {
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
match request.content_type() {
Some(content_type) => Success(content_type),
None => Forward(())
None => Forward(Status::NotFound)
}
}
}
@ -472,7 +478,7 @@ impl<'r> FromRequest<'r> for IpAddr {
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
match request.client_ip() {
Some(addr) => Success(addr),
None => Forward(())
None => Forward(Status::NotFound)
}
}
}
@ -484,7 +490,7 @@ impl<'r> FromRequest<'r> for SocketAddr {
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
match request.remote() {
Some(addr) => Success(addr),
None => Forward(())
None => Forward(Status::NotFound)
}
}
}
@ -497,7 +503,7 @@ impl<'r, T: FromRequest<'r>> FromRequest<'r> for Result<T, T::Error> {
match T::from_request(request).await {
Success(val) => Success(Ok(val)),
Failure((_, e)) => Success(Err(e)),
Forward(_) => Forward(()),
Forward(_) => Forward(Status::NotFound),
}
}
}

View File

@ -4,7 +4,7 @@ use crate::http::Status;
/// Type alias for the return type of a [`Route`](crate::Route)'s
/// [`Handler::handle()`].
pub type Outcome<'r> = crate::outcome::Outcome<Response<'r>, Status, Data<'r>>;
pub type Outcome<'r> = crate::outcome::Outcome<Response<'r>, Status, (Data<'r>, Status)>;
/// Type alias for the return type of a _raw_ [`Route`](crate::Route)'s
/// [`Handler`].
@ -239,7 +239,7 @@ impl<'r, 'o: 'r> Outcome<'o> {
{
match responder.respond_to(req) {
Ok(response) => Outcome::Success(response),
Err(_) => Outcome::Forward(data)
Err(_) => Outcome::Forward((data, Status::NotFound))
}
}
@ -264,7 +264,7 @@ impl<'r, 'o: 'r> Outcome<'o> {
}
/// Return an `Outcome` of `Forward` with the data `data`. This is
/// equivalent to `Outcome::Forward(data)`.
/// equivalent to `Outcome::Forward((data, Status::NotFound))`.
///
/// This method exists to be used during manual routing.
///
@ -279,7 +279,7 @@ impl<'r, 'o: 'r> Outcome<'o> {
/// ```
#[inline(always)]
pub fn forward(data: Data<'r>) -> Outcome<'r> {
Outcome::Forward(data)
Outcome::Forward((data, Status::NotFound))
}
}

View File

@ -283,7 +283,7 @@ impl Rocket<Orbit> {
) -> Response<'r> {
let mut response = match self.route(request, data).await {
Outcome::Success(response) => response,
Outcome::Forward(data) if request.method() == Method::Head => {
Outcome::Forward((data, _)) if request.method() == Method::Head => {
info_!("Autohandling {} request.", Paint::default("HEAD").bold());
// Dispatch the request again with Method `GET`.
@ -291,10 +291,10 @@ impl Rocket<Orbit> {
match self.route(request, data).await {
Outcome::Success(response) => response,
Outcome::Failure(status) => self.handle_error(status, request).await,
Outcome::Forward(_) => self.handle_error(Status::NotFound, request).await,
Outcome::Forward((_, status)) => self.handle_error(status, request).await,
}
}
Outcome::Forward(_) => self.handle_error(Status::NotFound, request).await,
Outcome::Forward((_, status)) => self.handle_error(status, request).await,
Outcome::Failure(status) => self.handle_error(status, request).await,
};
@ -319,7 +319,9 @@ impl Rocket<Orbit> {
request: &'r Request<'s>,
mut data: Data<'r>,
) -> route::Outcome<'r> {
// Go through the list of matching routes until we fail or succeed.
// Go through all matching routes until we fail or succeed or run out of
// routes to try, in which case we forward with the last status.
let mut status = Status::NotFound;
for route in self.router.route(request) {
// Retrieve and set the requests parameters.
info_!("Matched: {}", route);
@ -335,12 +337,12 @@ impl Rocket<Orbit> {
info_!("{} {}", Paint::default("Outcome:").bold(), outcome);
match outcome {
o@Outcome::Success(_) | o@Outcome::Failure(_) => return o,
Outcome::Forward(unused_data) => data = unused_data,
Outcome::Forward(forwarded) => (data, status) = forwarded,
}
}
error_!("No matching routes for {}.", request);
Outcome::Forward(data)
Outcome::Forward((data, status))
}
/// Invokes the handler with `req` for catcher with status `status`.

View File

@ -63,6 +63,7 @@ use crate::http::Status;
/// use rocket::State;
/// use rocket::request::{self, Request, FromRequest};
/// use rocket::outcome::IntoOutcome;
/// use rocket::http::Status;
///
/// # struct MyConfig { user_val: String };
/// struct Item<'r>(&'r str);
@ -79,7 +80,7 @@ use crate::http::Status;
/// // Or alternatively, using `Rocket::state()`:
/// let outcome = request.rocket().state::<MyConfig>()
/// .map(|my_config| Item(&my_config.user_val))
/// .or_forward(());
/// .or_forward(Status::NotFound);
///
/// outcome
/// }

View File

@ -0,0 +1,79 @@
#[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<Self, Self::Error> {
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);
}
}

View File

@ -3,6 +3,7 @@
use rocket::{Request, Data};
use rocket::request::{self, FromRequest};
use rocket::outcome::IntoOutcome;
use rocket::http::Status;
struct HasContentType;
@ -11,7 +12,7 @@ impl<'r> FromRequest<'r> for HasContentType {
type Error = ();
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, ()> {
req.content_type().map(|_| HasContentType).or_forward(())
req.content_type().map(|_| HasContentType).or_forward(Status::NotFound)
}
}
@ -22,7 +23,7 @@ impl<'r> FromData<'r> for HasContentType {
type Error = ();
async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> data::Outcome<'r, Self> {
req.content_type().map(|_| HasContentType).or_forward(data)
req.content_type().map(|_| HasContentType).or_forward((data, Status::NotFound))
}
}

View File

@ -1,7 +1,7 @@
use rocket::outcome::IntoOutcome;
use rocket::request::{self, FlashMessage, FromRequest, Request};
use rocket::response::{Redirect, Flash};
use rocket::http::{Cookie, CookieJar};
use rocket::http::{Cookie, CookieJar, Status};
use rocket::form::Form;
use rocket_dyn_templates::{Template, context};
@ -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(())
.or_forward(Status::NotFound)
}
}

View File

@ -84,7 +84,7 @@ impl route::Handler for CustomHandler {
let self_data = self.data;
let id = req.param::<&str>(0)
.and_then(Result::ok)
.or_forward(data);
.or_forward((data, Status::NotFound));
route::Outcome::from(req, format!("{} - {}", self_data, try_outcome!(id)))
}

View File

@ -124,6 +124,7 @@ retrieves `MyConfig` from managed state using both methods:
use rocket::State;
use rocket::request::{self, Request, FromRequest};
use rocket::outcome::IntoOutcome;
use rocket::http::Status;
# struct MyConfig { user_val: String };
struct Item<'r>(&'r str);
@ -140,7 +141,7 @@ impl<'r> FromRequest<'r> for Item<'r> {
// Or alternatively, using `Rocket::state()`:
let outcome = request.rocket().state::<MyConfig>()
.map(|my_config| Item(&my_config.user_val))
.or_forward(());
.or_forward(Status::NotFound);
outcome
}