Improve forwarding status code precision.

Previously, the `NotFound` status code was used to signal many kinds of
recoverable, forwarding errors. This included validation errors, incorrect
Content-Type errors, and more.

This commit modifies the status code used to forward in these instances to more
precisely indicate the forwarding condition. In particular:

  * Parameter `FromParam` errors now forward as 422 (`UnprocessableEntity`).
  * Query paramater errors now forward as 422 (`UnprocessableEntity`).
  * Use of incorrect form content-type forwards as 413 (`UnsupportedMediaType`).
  * `WebSocket` guard now forwards as 400 (`BadRequest`).
  * `&Host`, `&Accept`, `&ContentType`, `IpAddr`, and `SocketAddr` all forward
    with a 500 (`InternalServerError`).

Additionally, the `IntoOutcome` trait was overhauled to support functionality
previously offered by methods on `Outcome`. The `Outcome::forward()` method now
requires a status code to use for the forwarding outcome.

Finally, logging of `Outcome`s now includes the relevant status code.

Resolves #2626.
This commit is contained in:
Sergio Benitez 2023-10-31 18:27:03 -05:00
parent c90812051e
commit fbd1a0d069
19 changed files with 233 additions and 173 deletions

View File

@ -210,7 +210,7 @@ impl<'r, K: 'static, C: Poolable> FromRequest<'r> for Connection<K, C> {
#[inline] #[inline]
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, ()> { async fn from_request(request: &'r Request<'_>) -> Outcome<Self, ()> {
match request.rocket().state::<ConnectionPool<K, C>>() { match request.rocket().state::<ConnectionPool<K, C>>() {
Some(c) => c.get().await.into_outcome((Status::ServiceUnavailable, ())), Some(c) => c.get().await.or_error((Status::ServiceUnavailable, ())),
None => { None => {
error_!("Missing database fairing for `{}`", std::any::type_name::<K>()); error_!("Missing database fairing for `{}`", std::any::type_name::<K>());
Outcome::Error((Status::InternalServerError, ())) Outcome::Error((Status::InternalServerError, ()))

View File

@ -30,7 +30,7 @@ use crate::result::{Result, Error};
/// ### Forwarding /// ### Forwarding
/// ///
/// If the incoming request is not a valid WebSocket request, the guard /// If the incoming request is not a valid WebSocket request, the guard
/// forwards. The guard never fails. /// forwards with a status of `BadRequest`. The guard never fails.
pub struct WebSocket { pub struct WebSocket {
config: Config, config: Config,
key: String, key: String,
@ -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())); let key = headers.get_one("Sec-WebSocket-Key").map(|k| derive_accept_key(k.as_bytes()));
match key { match key {
Some(key) if is_upgrade && is_ws && is_13 => Outcome::Success(WebSocket::new(key)), Some(key) if is_upgrade && is_ws && is_13 => Outcome::Success(WebSocket::new(key)),
Some(_) | None => Outcome::Forward(Status::NotFound) Some(_) | None => Outcome::Forward(Status::BadRequest)
} }
} }
} }

View File

@ -105,7 +105,7 @@ fn query_decls(route: &Route) -> Option<TokenStream> {
if !__e.is_empty() { if !__e.is_empty() {
#_log::warn_!("Query string failed to match route declaration."); #_log::warn_!("Query string failed to match route declaration.");
for _err in __e { #_log::warn_!("{}", _err); } for _err in __e { #_log::warn_!("{}", _err); }
return #Outcome::Forward((#__data, #Status::NotFound)); return #Outcome::Forward((#__data, #Status::UnprocessableEntity));
} }
(#(#ident.unwrap()),*) (#(#ident.unwrap()),*)
@ -146,7 +146,7 @@ fn param_guard_decl(guard: &Guard) -> TokenStream {
#_log::warn_!("Parameter guard `{}: {}` is forwarding: {:?}.", #_log::warn_!("Parameter guard `{}: {}` is forwarding: {:?}.",
#name, stringify!(#ty), __error); #name, stringify!(#ty), __error);
#Outcome::Forward((#__data, #Status::NotFound)) #Outcome::Forward((#__data, #Status::UnprocessableEntity))
}); });
// All dynamic parameters should be found if this function is being called; // All dynamic parameters should be found if this function is being called;

View File

@ -269,7 +269,7 @@ fn test_query_collection() {
let colors = &["red"]; let colors = &["red"];
let dog = &["name=Fido"]; let dog = &["name=Fido"];
assert_eq!(run(&client, colors, dog).0, Status::NotFound); assert_eq!(run(&client, colors, dog).0, Status::UnprocessableEntity);
let colors = &["red"]; let colors = &["red"];
let dog = &["name=Fido", "age=2"]; let dog = &["name=Fido", "age=2"];

View File

@ -112,7 +112,7 @@ impl<'r> Data<'r> {
/// ///
/// async fn from_data(r: &'r Request<'_>, mut data: Data<'r>) -> Outcome<'r, Self> { /// async fn from_data(r: &'r Request<'_>, mut data: Data<'r>) -> Outcome<'r, Self> {
/// if data.peek(2).await != b"hi" { /// if data.peek(2).await != b"hi" {
/// return Outcome::Forward((data, Status::NotFound)) /// return Outcome::Forward((data, Status::BadRequest))
/// } /// }
/// ///
/// /* .. */ /// /* .. */

View File

@ -9,27 +9,6 @@ use crate::outcome::{self, IntoOutcome, try_outcome, Outcome::*};
pub type Outcome<'r, T, E = <T as FromData<'r>>::Error> pub type Outcome<'r, T, E = <T as FromData<'r>>::Error>
= outcome::Outcome<T, (Status, E), (Data<'r>, Status)>; = outcome::Outcome<T, (Status, E), (Data<'r>, Status)>;
impl<'r, S, E> IntoOutcome<S, (Status, E), (Data<'r>, Status)> for Result<S, E> {
type Error = Status;
type Forward = (Data<'r>, Status);
#[inline]
fn into_outcome(self, status: Status) -> Outcome<'r, S, E> {
match self {
Ok(val) => Success(val),
Err(err) => Error((status, err))
}
}
#[inline]
fn or_forward(self, (data, status): (Data<'r>, Status)) -> Outcome<'r, S, E> {
match self {
Ok(val) => Success(val),
Err(_) => Forward((data, status))
}
}
}
/// Trait implemented by data guards to derive a value from request body data. /// Trait implemented by data guards to derive a value from request body data.
/// ///
/// # Data Guards /// # Data Guards
@ -271,7 +250,7 @@ impl<'r, S, E> IntoOutcome<S, (Status, E), (Data<'r>, Status)> for Result<S, E>
/// // Ensure the content type is correct before opening the data. /// // Ensure the content type is correct before opening the data.
/// let person_ct = ContentType::new("application", "x-person"); /// let person_ct = ContentType::new("application", "x-person");
/// if req.content_type() != Some(&person_ct) { /// if req.content_type() != Some(&person_ct) {
/// return Outcome::Forward((data, Status::NotFound)); /// return Outcome::Forward((data, Status::UnsupportedMediaType));
/// } /// }
/// ///
/// // Use a configured limit with name 'person' or fallback to default. /// // Use a configured limit with name 'person' or fallback to default.
@ -343,7 +322,7 @@ impl<'r> FromData<'r> for Capped<String> {
async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r, Self> { async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r, Self> {
let limit = req.limits().get("string").unwrap_or(Limits::STRING); let limit = req.limits().get("string").unwrap_or(Limits::STRING);
data.open(limit).into_string().await.into_outcome(Status::BadRequest) data.open(limit).into_string().await.or_error(Status::BadRequest)
} }
} }
@ -406,7 +385,7 @@ impl<'r> FromData<'r> for Capped<Vec<u8>> {
async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r, Self> { async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r, Self> {
let limit = req.limits().get("bytes").unwrap_or(Limits::BYTES); let limit = req.limits().get("bytes").unwrap_or(Limits::BYTES);
data.open(limit).into_bytes().await.into_outcome(Status::BadRequest) data.open(limit).into_bytes().await.or_error(Status::BadRequest)
} }
} }

View File

@ -35,7 +35,7 @@ impl<'r, 'i> Parser<'r, 'i> {
let parser = match req.content_type() { let parser = match req.content_type() {
Some(c) if c.is_form() => Self::from_form(req, data).await, 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, Some(c) if c.is_form_data() => Self::from_multipart(req, data).await,
_ => return Outcome::Forward((data, Status::NotFound)), _ => return Outcome::Forward((data, Status::UnsupportedMediaType)),
}; };
match parser { match parser {

View File

@ -1,9 +1,10 @@
use std::path::{PathBuf, Path}; use std::path::{PathBuf, Path};
use crate::{Request, Data}; use crate::{Request, Data};
use crate::http::{Method, uri::Segments, ext::IntoOwned}; use crate::http::{Method, Status, uri::Segments, ext::IntoOwned};
use crate::route::{Route, Handler, Outcome}; use crate::route::{Route, Handler, Outcome};
use crate::response::Redirect; use crate::response::{Redirect, Responder};
use crate::outcome::IntoOutcome;
use crate::fs::NamedFile; use crate::fs::NamedFile;
/// Custom handler for serving static files. /// Custom handler for serving static files.
@ -203,10 +204,10 @@ impl Handler for FileServer {
}; };
if segments.is_empty() { if segments.is_empty() {
let file = NamedFile::open(&self.root).await.ok(); let file = NamedFile::open(&self.root).await;
return Outcome::from_or_forward(req, data, file); return file.respond_to(req).or_forward((data, Status::NotFound));
} else { } else {
return Outcome::forward(data); return Outcome::forward(data, Status::NotFound);
} }
} }
@ -224,18 +225,23 @@ impl Handler for FileServer {
.expect("adding a trailing slash to a known good path => valid path") .expect("adding a trailing slash to a known good path => valid path")
.into_owned(); .into_owned();
return Outcome::from_or_forward(req, data, Redirect::permanent(normal)); return Redirect::permanent(normal)
.respond_to(req)
.or_forward((data, Status::InternalServerError));
} }
if !options.contains(Options::Index) { if !options.contains(Options::Index) {
return Outcome::forward(data); return Outcome::forward(data, Status::NotFound);
} }
let index = NamedFile::open(p.join("index.html")).await.ok(); let index = NamedFile::open(p.join("index.html")).await;
Outcome::from_or_forward(req, data, index) index.respond_to(req).or_forward((data, Status::NotFound))
}, },
Some(p) => Outcome::from_or_forward(req, data, NamedFile::open(p).await.ok()), Some(p) => {
None => Outcome::forward(data), let file = NamedFile::open(p).await;
file.respond_to(req).or_forward((data, Status::NotFound))
}
None => Outcome::forward(data, Status::NotFound),
} }
} }
} }

View File

@ -543,7 +543,7 @@ impl<'r> FromData<'r> for Capped<TempFile<'_>> {
} }
TempFile::from(req, data, None, req.content_type().cloned()).await TempFile::from(req, data, None, req.content_type().cloned()).await
.into_outcome(Status::BadRequest) .or_error(Status::BadRequest)
} }
} }

View File

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

View File

@ -90,6 +90,10 @@ use std::fmt;
use yansi::{Paint, Color}; use yansi::{Paint, Color};
use crate::{route, request, response};
use crate::data::{self, Data, FromData};
use crate::http::Status;
use self::Outcome::*; use self::Outcome::*;
/// An enum representing success (`Success`), error (`Error`), or forwarding /// An enum representing success (`Success`), error (`Error`), or forwarding
@ -107,46 +111,6 @@ pub enum Outcome<S, E, F> {
Forward(F), Forward(F),
} }
/// Conversion trait from some type into an Outcome type.
pub trait IntoOutcome<S, E, F> {
/// The type to use when returning an `Outcome::Error`.
type Error: Sized;
/// The type to use when returning an `Outcome::Forward`.
type Forward: Sized;
/// Converts `self` into an `Outcome`. If `self` represents a success, an
/// `Outcome::Success` is returned. Otherwise, an `Outcome::Error` is
/// returned with `error` as the inner value.
fn into_outcome(self, error: Self::Error) -> Outcome<S, E, F>;
/// Converts `self` into an `Outcome`. If `self` represents a success, an
/// `Outcome::Success` is returned. Otherwise, an `Outcome::Forward` is
/// returned with `forward` as the inner value.
fn or_forward(self, forward: Self::Forward) -> Outcome<S, E, F>;
}
impl<S, E, F> IntoOutcome<S, E, F> for Option<S> {
type Error = E;
type Forward = F;
#[inline]
fn into_outcome(self, error: E) -> Outcome<S, E, F> {
match self {
Some(val) => Success(val),
None => Error(error)
}
}
#[inline]
fn or_forward(self, forward: F) -> Outcome<S, E, F> {
match self {
Some(val) => Success(val),
None => Forward(forward)
}
}
}
impl<S, E, F> Outcome<S, E, F> { impl<S, E, F> Outcome<S, E, F> {
/// Unwraps the Outcome, yielding the contents of a Success. /// Unwraps the Outcome, yielding the contents of a Success.
/// ///
@ -651,15 +615,6 @@ impl<S, E, F> Outcome<S, E, F> {
Outcome::Forward(v) => Err(v), Outcome::Forward(v) => Err(v),
} }
} }
#[inline]
fn formatting(&self) -> (Color, &'static str) {
match *self {
Success(..) => (Color::Green, "Success"),
Error(..) => (Color::Red, "Error"),
Forward(..) => (Color::Yellow, "Forward"),
}
}
} }
impl<'a, S: Send + 'a, E: Send + 'a, F: Send + 'a> Outcome<S, E, F> { impl<'a, S: Send + 'a, E: Send + 'a, F: Send + 'a> Outcome<S, E, F> {
@ -755,15 +710,158 @@ crate::export! {
} }
} }
impl<S, E, F> Outcome<S, E, F> {
#[inline]
fn dbg_str(&self) -> &'static str {
match self {
Success(..) => "Success",
Error(..) => "Error",
Forward(..) => "Forward",
}
}
#[inline]
fn color(&self) -> Color {
match self {
Success(..) => Color::Green,
Error(..) => Color::Red,
Forward(..) => Color::Yellow,
}
}
}
pub(crate) struct Display<'a, 'r>(&'a route::Outcome<'r>);
impl<'r> route::Outcome<'r> {
pub(crate) fn log_display(&self) -> Display<'_, 'r> {
impl fmt::Display for Display<'_, '_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", "Outcome: ".primary().bold())?;
let color = self.0.color();
match self.0 {
Success(r) => write!(f, "{}({})", "Success".paint(color), r.status().primary()),
Error(s) => write!(f, "{}({})", "Error".paint(color), s.primary()),
Forward((_, s)) => write!(f, "{}({})", "Forward".paint(color), s.primary()),
}
}
}
Display(self)
}
}
impl<S, E, F> fmt::Debug for Outcome<S, E, F> { impl<S, E, F> fmt::Debug for Outcome<S, E, F> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Outcome::{}", self.formatting().1) write!(f, "Outcome::{}", self.dbg_str())
} }
} }
impl<S, E, F> fmt::Display for Outcome<S, E, F> { impl<S, E, F> fmt::Display for Outcome<S, E, F> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let (color, string) = self.formatting(); write!(f, "{}", self.dbg_str().paint(self.color()))
write!(f, "{}", string.paint(color)) }
}
/// Conversion trait from some type into an Outcome type.
pub trait IntoOutcome<Outcome> {
/// The type to use when returning an `Outcome::Error`.
type Error: Sized;
/// The type to use when returning an `Outcome::Forward`.
type Forward: Sized;
/// Converts `self` into an `Outcome`. If `self` represents a success, an
/// `Outcome::Success` is returned. Otherwise, an `Outcome::Error` is
/// returned with `error` as the inner value.
fn or_error(self, error: Self::Error) -> Outcome;
/// Converts `self` into an `Outcome`. If `self` represents a success, an
/// `Outcome::Success` is returned. Otherwise, an `Outcome::Forward` is
/// returned with `forward` as the inner value.
fn or_forward(self, forward: Self::Forward) -> Outcome;
}
impl<S, E, F> IntoOutcome<Outcome<S, E, F>> for Option<S> {
type Error = E;
type Forward = F;
#[inline]
fn or_error(self, error: E) -> Outcome<S, E, F> {
match self {
Some(val) => Success(val),
None => Error(error)
}
}
#[inline]
fn or_forward(self, forward: F) -> Outcome<S, E, F> {
match self {
Some(val) => Success(val),
None => Forward(forward)
}
}
}
impl<'r, T: FromData<'r>> IntoOutcome<data::Outcome<'r, T>> for Result<T, T::Error> {
type Error = Status;
type Forward = (Data<'r>, Status);
#[inline]
fn or_error(self, error: Status) -> data::Outcome<'r, T> {
match self {
Ok(val) => Success(val),
Err(err) => Error((error, err))
}
}
#[inline]
fn or_forward(self, (data, forward): (Data<'r>, Status)) -> data::Outcome<'r, T> {
match self {
Ok(val) => Success(val),
Err(_) => Forward((data, forward))
}
}
}
impl<S, E> IntoOutcome<request::Outcome<S, E>> for Result<S, E> {
type Error = Status;
type Forward = Status;
#[inline]
fn or_error(self, error: Status) -> request::Outcome<S, E> {
match self {
Ok(val) => Success(val),
Err(err) => Error((error, err))
}
}
#[inline]
fn or_forward(self, status: Status) -> request::Outcome<S, E> {
match self {
Ok(val) => Success(val),
Err(_) => Forward(status)
}
}
}
impl<'r, 'o: 'r> IntoOutcome<route::Outcome<'r>> for response::Result<'o> {
type Error = ();
type Forward = (Data<'r>, Status);
#[inline]
fn or_error(self, _: ()) -> route::Outcome<'r> {
match self {
Ok(val) => Success(val),
Err(status) => Error(status),
}
}
#[inline]
fn or_forward(self, (data, forward): (Data<'r>, Status)) -> route::Outcome<'r> {
match self {
Ok(val) => Success(val),
Err(_) => Forward((data, forward))
}
} }
} }

View File

@ -3,36 +3,14 @@ use std::fmt::Debug;
use std::net::{IpAddr, SocketAddr}; use std::net::{IpAddr, SocketAddr};
use crate::{Request, Route}; use crate::{Request, Route};
use crate::outcome::{self, IntoOutcome}; use crate::outcome::{self, Outcome::*};
use crate::outcome::Outcome::*;
use crate::http::{Status, ContentType, Accept, Method, CookieJar};
use crate::http::uri::{Host, Origin}; use crate::http::uri::{Host, Origin};
use crate::http::{Status, ContentType, Accept, Method, CookieJar};
/// Type alias for the `Outcome` of a `FromRequest` conversion. /// Type alias for the `Outcome` of a `FromRequest` conversion.
pub type Outcome<S, E> = outcome::Outcome<S, (Status, E), Status>; pub type Outcome<S, E> = outcome::Outcome<S, (Status, E), Status>;
impl<S, E> IntoOutcome<S, (Status, E), Status> for Result<S, E> {
type Error = Status;
type Forward = Status;
#[inline]
fn into_outcome(self, status: Status) -> Outcome<S, E> {
match self {
Ok(val) => Success(val),
Err(err) => Error((status, err))
}
}
#[inline]
fn or_forward(self, status: Status) -> Outcome<S, E> {
match self {
Ok(val) => Success(val),
Err(_) => Forward(status)
}
}
}
/// Trait implemented by request guards to derive a value from incoming /// Trait implemented by request guards to derive a value from incoming
/// requests. /// requests.
/// ///
@ -136,7 +114,8 @@ impl<S, E> IntoOutcome<S, (Status, E), Status> for Result<S, E> {
/// * **&Host** /// * **&Host**
/// ///
/// Extracts the [`Host`] from the incoming request, if it exists. See /// Extracts the [`Host`] from the incoming request, if it exists. See
/// [`Request::host()`] for details. /// [`Request::host()`] for details. If it does not exist, the request is
/// forwarded with a 500 Internal Server Error status.
/// ///
/// * **&Route** /// * **&Route**
/// ///
@ -162,23 +141,30 @@ impl<S, E> IntoOutcome<S, (Status, E), Status> for Result<S, E> {
/// ///
/// _This implementation always returns successfully._ /// _This implementation always returns successfully._
/// ///
/// * **ContentType** /// * **&ContentType**
/// ///
/// Extracts the [`ContentType`] from the incoming request via /// Extracts the [`ContentType`] from the incoming request via
/// [`Request::content_type()`]. If the request didn't specify a /// [`Request::content_type()`]. If the request didn't specify a
/// Content-Type, the request is forwarded with a 404 Not Found status. /// Content-Type, the request is forwarded with a 500 Internal Server Error
/// status.
/// ///
/// * **IpAddr** /// * **&ContentType**
///
/// Extracts the [`Accept`] from the incoming request via
/// [`Request::accept()`]. If the request didn't specify an `Accept`, the
/// request is forwarded with a 500 Internal Server Error status.
///
/// * ***IpAddr**
/// ///
/// Extracts the client ip address of the incoming request as an [`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, /// via [`Request::client_ip()`]. If the client's IP address is not known,
/// the request is forwarded with a 404 Not Found status. /// the request is forwarded with a 500 Internal Server Error status.
/// ///
/// * **SocketAddr** /// * **SocketAddr**
/// ///
/// Extracts the remote address of the incoming request as a [`SocketAddr`] /// Extracts the remote address of the incoming request as a [`SocketAddr`]
/// via [`Request::remote()`]. If the remote address is not known, the /// via [`Request::remote()`]. If the remote address is not known, the
/// request is forwarded with a 404 Not Found status. /// request is forwarded with a 500 Internal Server Error status.
/// ///
/// * **Option&lt;T>** _where_ **T: FromRequest** /// * **Option&lt;T>** _where_ **T: FromRequest**
/// ///
@ -422,7 +408,7 @@ impl<'r> FromRequest<'r> for &'r Host<'r> {
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Infallible> { async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Infallible> {
match request.host() { match request.host() {
Some(host) => Success(host), Some(host) => Success(host),
None => Forward(Status::NotFound) None => Forward(Status::InternalServerError)
} }
} }
} }
@ -455,7 +441,7 @@ impl<'r> FromRequest<'r> for &'r Accept {
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Infallible> { async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Infallible> {
match request.accept() { match request.accept() {
Some(accept) => Success(accept), Some(accept) => Success(accept),
None => Forward(Status::NotFound) None => Forward(Status::InternalServerError)
} }
} }
} }
@ -467,7 +453,7 @@ impl<'r> FromRequest<'r> for &'r ContentType {
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Infallible> { async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Infallible> {
match request.content_type() { match request.content_type() {
Some(content_type) => Success(content_type), Some(content_type) => Success(content_type),
None => Forward(Status::NotFound) None => Forward(Status::InternalServerError)
} }
} }
} }
@ -479,7 +465,7 @@ impl<'r> FromRequest<'r> for IpAddr {
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Infallible> { async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Infallible> {
match request.client_ip() { match request.client_ip() {
Some(addr) => Success(addr), Some(addr) => Success(addr),
None => Forward(Status::NotFound) None => Forward(Status::InternalServerError)
} }
} }
} }
@ -491,7 +477,7 @@ impl<'r> FromRequest<'r> for SocketAddr {
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Infallible> { async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Infallible> {
match request.remote() { match request.remote() {
Some(addr) => Success(addr), Some(addr) => Success(addr),
None => Forward(Status::NotFound) None => Forward(Status::InternalServerError)
} }
} }
} }

View File

@ -259,7 +259,7 @@ impl<'r> FromRequest<'r> for FlashMessage<'r> {
Ok(i) if i <= kv.len() => Ok(Flash::named(&kv[..i], &kv[i..], req)), Ok(i) if i <= kv.len() => Ok(Flash::named(&kv[..i], &kv[i..], req)),
_ => Err(()) _ => Err(())
} }
}).into_outcome(Status::BadRequest) }).or_error(Status::BadRequest)
} }
} }

View File

@ -218,31 +218,6 @@ 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`
/// with a status of `404 Not Found` is returned.
///
/// # Example
///
/// ```rust
/// use rocket::{Request, Data, route};
///
/// fn str_responder<'r>(req: &'r Request, data: Data<'r>) -> route::Outcome<'r> {
/// route::Outcome::from_or_forward(req, data, "Hello, world!")
/// }
/// ```
#[inline]
pub fn from_or_forward<R>(req: &'r Request<'_>, data: Data<'r>, responder: R) -> Outcome<'r>
where R: Responder<'r, 'o>
{
match responder.respond_to(req) {
Ok(response) => Outcome::Success(response),
Err(_) => Outcome::Forward((data, Status::NotFound))
}
}
/// Return an `Outcome` of `Error` with the status code `code`. This is /// Return an `Outcome` of `Error` with the status code `code`. This is
/// equivalent to `Outcome::Error(code)`. /// equivalent to `Outcome::Error(code)`.
/// ///
@ -263,8 +238,8 @@ impl<'r, 'o: 'r> Outcome<'o> {
Outcome::Error(code) Outcome::Error(code)
} }
/// Return an `Outcome` of `Forward` with the data `data`. This is /// Return an `Outcome` of `Forward` with the data `data` and status
/// equivalent to `Outcome::Forward((data, Status::NotFound))`. /// `status`. This is equivalent to `Outcome::Forward((data, status))`.
/// ///
/// This method exists to be used during manual routing. /// This method exists to be used during manual routing.
/// ///
@ -272,14 +247,15 @@ impl<'r, 'o: 'r> Outcome<'o> {
/// ///
/// ```rust /// ```rust
/// use rocket::{Request, Data, route}; /// use rocket::{Request, Data, route};
/// use rocket::http::Status;
/// ///
/// fn always_forward<'r>(_: &'r Request, data: Data<'r>) -> route::Outcome<'r> { /// fn always_forward<'r>(_: &'r Request, data: Data<'r>) -> route::Outcome<'r> {
/// route::Outcome::forward(data) /// route::Outcome::forward(data, Status::InternalServerError)
/// } /// }
/// ``` /// ```
#[inline(always)] #[inline(always)]
pub fn forward(data: Data<'r>) -> Outcome<'r> { pub fn forward(data: Data<'r>, status: Status) -> Outcome<'r> {
Outcome::Forward((data, Status::NotFound)) Outcome::Forward((data, status))
} }
} }

View File

@ -334,7 +334,7 @@ impl Rocket<Orbit> {
// Check if the request processing completed (Some) or if the // Check if the request processing completed (Some) or if the
// request needs to be forwarded. If it does, continue the loop // request needs to be forwarded. If it does, continue the loop
// (None) to try again. // (None) to try again.
info_!("{} {}", "Outcome:".primary().bold(), outcome); info_!("{}", outcome.log_display());
match outcome { match outcome {
o@Outcome::Success(_) | o@Outcome::Error(_) => return o, o@Outcome::Success(_) | o@Outcome::Error(_) => return o,
Outcome::Forward(forwarded) => (data, status) = forwarded, Outcome::Forward(forwarded) => (data, status) = forwarded,

View File

@ -46,7 +46,15 @@ fn forced_error() {
fn test_hello_invalid_age() { fn test_hello_invalid_age() {
let client = Client::tracked(super::rocket()).unwrap(); let client = Client::tracked(super::rocket()).unwrap();
for path in &["Ford/-129", "Trillian/128", "foo/bar/baz"] { for path in &["Ford/-129", "Trillian/128"] {
let request = client.get(format!("/hello/{}", path));
let expected = super::default_catcher(Status::UnprocessableEntity, request.inner());
let response = request.dispatch();
assert_eq!(response.status(), Status::UnprocessableEntity);
assert_eq!(response.into_string().unwrap(), expected.1);
}
for path in &["foo/bar/baz"] {
let request = client.get(format!("/hello/{}", path)); let request = client.get(format!("/hello/{}", path));
let expected = super::hello_not_found(request.inner()); let expected = super::hello_not_found(request.inner());
let response = request.dispatch(); let response = request.dispatch();
@ -59,7 +67,15 @@ fn test_hello_invalid_age() {
fn test_hello_sergio() { fn test_hello_sergio() {
let client = Client::tracked(super::rocket()).unwrap(); let client = Client::tracked(super::rocket()).unwrap();
for path in &["oops", "-129", "foo/bar", "/foo/bar/baz"] { for path in &["oops", "-129"] {
let request = client.get(format!("/hello/Sergio/{}", path));
let expected = super::sergio_error();
let response = request.dispatch();
assert_eq!(response.status(), Status::UnprocessableEntity);
assert_eq!(response.into_string().unwrap(), expected);
}
for path in &["foo/bar", "/foo/bar/baz"] {
let request = client.get(format!("/hello/Sergio/{}", path)); let request = client.get(format!("/hello/Sergio/{}", path));
let expected = super::sergio_error(); let expected = super::sergio_error();
let response = request.dispatch(); let response = request.dispatch();

View File

@ -62,10 +62,10 @@ fn wave() {
let response = client.get(uri).dispatch(); let response = client.get(uri).dispatch();
assert_eq!(response.into_string().unwrap(), expected); assert_eq!(response.into_string().unwrap(), expected);
for bad_age in &["1000", "-1", "bird", "?"] { for bad_age in &["1000", "-1", "bird"] {
let bad_uri = format!("/wave/{}/{}", name, bad_age); let bad_uri = format!("/wave/{}/{}", name, bad_age);
let response = client.get(bad_uri).dispatch(); let response = client.get(bad_uri).dispatch();
assert_eq!(response.status(), Status::NotFound); assert_eq!(response.status(), Status::UnprocessableEntity);
} }
} }
} }

View File

@ -9,7 +9,7 @@ use rocket::outcome::{try_outcome, IntoOutcome};
use rocket::tokio::fs::File; use rocket::tokio::fs::File;
fn forward<'r>(_req: &'r Request, data: Data<'r>) -> route::BoxFuture<'r> { fn forward<'r>(_req: &'r Request, data: Data<'r>) -> route::BoxFuture<'r> {
Box::pin(async move { route::Outcome::forward(data) }) Box::pin(async move { route::Outcome::forward(data, Status::NotFound) })
} }
fn hi<'r>(req: &'r Request, _: Data<'r>) -> route::BoxFuture<'r> { fn hi<'r>(req: &'r Request, _: Data<'r>) -> route::BoxFuture<'r> {
@ -27,7 +27,7 @@ fn name<'r>(req: &'r Request, _: Data<'r>) -> route::BoxFuture<'r> {
fn echo_url<'r>(req: &'r Request, _: Data<'r>) -> route::BoxFuture<'r> { fn echo_url<'r>(req: &'r Request, _: Data<'r>) -> route::BoxFuture<'r> {
let param_outcome = req.param::<&str>(1) let param_outcome = req.param::<&str>(1)
.and_then(Result::ok) .and_then(Result::ok)
.into_outcome(Status::BadRequest); .or_error(Status::BadRequest);
Box::pin(async move { Box::pin(async move {
route::Outcome::from(req, try_outcome!(param_outcome)) route::Outcome::from(req, try_outcome!(param_outcome))

View File

@ -34,8 +34,7 @@ fn json_bad_get_put() {
// Try to get a message with an invalid ID. // Try to get a message with an invalid ID.
let res = client.get("/json/hi").header(ContentType::JSON).dispatch(); let res = client.get("/json/hi").header(ContentType::JSON).dispatch();
assert_eq!(res.status(), Status::NotFound); assert_eq!(res.status(), Status::UnprocessableEntity);
assert!(res.into_string().unwrap().contains("error"));
// Try to put a message without a proper body. // Try to put a message without a proper body.
let res = client.put("/json/80").header(ContentType::JSON).dispatch(); let res = client.put("/json/80").header(ContentType::JSON).dispatch();
@ -134,5 +133,5 @@ fn uuid() {
} }
let res = client.get("/people/not-a-uuid").dispatch(); let res = client.get("/people/not-a-uuid").dispatch();
assert_eq!(res.status(), Status::NotFound); assert_eq!(res.status(), Status::UnprocessableEntity);
} }