From 03433c10ea77ffc3e1a89821063f8643efdf9a1f Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 5 Apr 2023 09:56:49 -0700 Subject: [PATCH] Allow specifying 'Status' in custom form errors. Resolves #1694. --- core/http/src/header/media_type.rs | 2 +- core/lib/src/form/error.rs | 49 ++++++++++++++++++++---------- core/lib/src/form/form.rs | 20 ++++++++++++ core/lib/src/form/mod.rs | 3 +- 4 files changed, 56 insertions(+), 18 deletions(-) diff --git a/core/http/src/header/media_type.rs b/core/http/src/header/media_type.rs index e51fea81..b217dbd5 100644 --- a/core/http/src/header/media_type.rs +++ b/core/http/src/header/media_type.rs @@ -112,7 +112,7 @@ macro_rules! media_types { docify!([ Returns @code{true} if the @[top-level] and sublevel types of @code{self} are the same as those of @{"`MediaType::"}! $name - @{"`"}!. + @{"`"}!, i.e @{"`"} @{$t}! @[/]! @{$s}! $(; @{$k}! @[=]! @{$v}!)* @{"`"}!. ]; #[inline(always)] pub fn $check(&self) -> bool { diff --git a/core/lib/src/form/error.rs b/core/lib/src/form/error.rs index c550c149..7167eb5e 100644 --- a/core/lib/src/form/error.rs +++ b/core/lib/src/form/error.rs @@ -160,6 +160,7 @@ pub struct Error<'v> { /// * [`AddrParseError`] => [`ErrorKind::Addr`] /// * [`io::Error`] => [`ErrorKind::Io`] /// * `Box [`ErrorKind::Custom`] +/// * `(Status, Box [`ErrorKind::Custom`] #[derive(Debug)] #[non_exhaustive] pub enum ErrorKind<'v> { @@ -192,8 +193,9 @@ pub enum ErrorKind<'v> { Unexpected, /// An unknown entity was received. Unknown, - /// A custom error occurred. - Custom(Box), + /// A custom error occurred. Status defaults to + /// [`Status::UnprocessableEntity`] if one is not directly specified. + Custom(Status, Box), /// An error while parsing a multipart form occurred. Multipart(multer::Error), /// A string was invalid UTF-8. @@ -350,6 +352,9 @@ impl<'v> Errors<'v> { /// status that is set by the [`Form`](crate::form::Form) data guard on /// failure. /// + /// See [`Error::status()`] for the corresponding status code of each + /// [`Error`] variant. + /// /// # Example /// /// ```rust @@ -677,17 +682,19 @@ impl<'v> Error<'v> { .unwrap_or(false) } - /// Returns the most reasonable `Status` associated with this error. These - /// are: + /// Returns the most reasonable [`Status`] associated with this error. /// - /// * **`PayloadTooLarge`** if the error kind is: + /// For an [`ErrorKind::Custom`], this is the variant's `Status`, which + /// defaults to [`Status::UnprocessableEntity`]. For all others, it is: + /// + /// * **`PayloadTooLarge`** if the [error kind](ErrorKind) is: /// - `InvalidLength` with min of `None` - /// - `Multpart(FieldSizeExceeded | StreamSizeExceeded)` - /// * **`InternalServerError`** if the error kind is: + /// - `Multipart(FieldSizeExceeded)` or `Multipart(StreamSizeExceeded)` + /// * **`InternalServerError`** if the [error kind](ErrorKind) is: /// - `Unknown` - /// * **`BadRequest`** if the error kind is: + /// * **`BadRequest`** if the [error kind](ErrorKind) is: /// - `Io` with an `entity` of `Form` - /// * **`UnprocessableEntity`** otherwise + /// * **`UnprocessableEntity`** for all other variants /// /// # Example /// @@ -716,11 +723,12 @@ impl<'v> Error<'v> { use multer::Error::*; match self.kind { - InvalidLength { min: None, .. } + | InvalidLength { min: None, .. } | Multipart(FieldSizeExceeded { .. }) | Multipart(StreamSizeExceeded { .. }) => Status::PayloadTooLarge, Unknown => Status::InternalServerError, Io(_) | _ if self.entity == Entity::Form => Status::BadRequest, + Custom(status, _) => status, _ => Status::UnprocessableEntity } } @@ -846,7 +854,7 @@ impl fmt::Display for ErrorKind<'_> { ErrorKind::Missing => "missing".fmt(f)?, ErrorKind::Unexpected => "unexpected".fmt(f)?, ErrorKind::Unknown => "unknown internal error".fmt(f)?, - ErrorKind::Custom(e) => e.fmt(f)?, + ErrorKind::Custom(_, e) => e.fmt(f)?, ErrorKind::Multipart(e) => write!(f, "invalid multipart: {}", e)?, ErrorKind::Utf8(e) => write!(f, "invalid UTF-8: {}", e)?, ErrorKind::Int(e) => write!(f, "invalid integer: {}", e)?, @@ -874,7 +882,7 @@ impl crate::http::ext::IntoOwned for ErrorKind<'_> { Missing => Missing, Unexpected => Unexpected, Unknown => Unknown, - Custom(e) => Custom(e), + Custom(s, e) => Custom(s, e), Multipart(e) => Multipart(e), Utf8(e) => Utf8(e), Int(e) => Int(e), @@ -892,7 +900,6 @@ impl crate::http::ext::IntoOwned for ErrorKind<'_> { } } - impl<'a, 'b> PartialEq> for ErrorKind<'a> { fn eq(&self, other: &ErrorKind<'b>) -> bool { use ErrorKind::*; @@ -904,7 +911,7 @@ impl<'a, 'b> PartialEq> for ErrorKind<'a> { (Duplicate, Duplicate) => true, (Missing, Missing) => true, (Unexpected, Unexpected) => true, - (Custom(_), Custom(_)) => true, + (Custom(a, _), Custom(b, _)) => a == b, (Multipart(a), Multipart(b)) => a == b, (Utf8(a), Utf8(b)) => a == b, (Int(a), Int(b)) => a == b, @@ -954,6 +961,17 @@ impl<'a, 'v: 'a, const N: usize> From<&'static [Cow<'v, str>; N]> for ErrorKind< } } +impl<'a> From> for ErrorKind<'a> { + fn from(e: Box) -> Self { + ErrorKind::Custom(Status::UnprocessableEntity, e) + } +} + +impl<'a> From<(Status, Box)> for ErrorKind<'a> { + fn from((status, e): (Status, Box)) -> Self { + ErrorKind::Custom(status, e) + } +} macro_rules! impl_from_for { (<$l:lifetime> $T:ty => $V:ty as $variant:ident) => ( @@ -971,7 +989,6 @@ impl_from_for!(<'a> ParseFloatError => ErrorKind<'a> as Float); impl_from_for!(<'a> ParseBoolError => ErrorKind<'a> as Bool); impl_from_for!(<'a> AddrParseError => ErrorKind<'a> as Addr); impl_from_for!(<'a> io::Error => ErrorKind<'a> as Io); -impl_from_for!(<'a> Box => ErrorKind<'a> as Custom); impl fmt::Display for Entity { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -1010,7 +1027,7 @@ impl Entity { | ErrorKind::Int(_) | ErrorKind::Float(_) | ErrorKind::Bool(_) - | ErrorKind::Custom(_) + | ErrorKind::Custom(..) | ErrorKind::Addr(_) => Entity::Value, | ErrorKind::Duplicate diff --git a/core/lib/src/form/form.rs b/core/lib/src/form/form.rs index 4d96a5cd..c747adc2 100644 --- a/core/lib/src/form/form.rs +++ b/core/lib/src/form/form.rs @@ -56,6 +56,26 @@ use crate::form::prelude::*; /// can access fields of `T` transparently through a `Form`, as seen above /// with `user_input.value`. /// +/// ## Errors +/// +/// A `Form` data guard may fail, forward, or succeed. +/// +/// If a request's content-type is neither [`ContentType::Form`] nor +/// [`ContentType::FormData`], the guard **forwards**. +/// +/// If the request `ContentType` _does_ identify as a form but the form data +/// does not parse as `T`, according to `T`'s [`FromForm`] implementation, the +/// guard **fails**. The `Failure` variant contains of the [`Errors`] emitted by +/// `T`'s `FromForm` parser. If the error is not caught by a +/// [`form::Result`](Result) or `Option>` data guard, the status code +/// is set to [`Errors::status()`], and the corresponding error catcher is +/// called. +/// +/// Otherwise the guard **succeeds**. +/// +/// [`ContentType::Form`]: crate::http::ContentType::Form +/// [`ContentType::FormData`]: crate::http::ContentType::FormData +/// /// ## Data Limits /// /// The total amount of data accepted by the `Form` data guard is limited by the diff --git a/core/lib/src/form/mod.rs b/core/lib/src/form/mod.rs index 739c5185..442c455a 100644 --- a/core/lib/src/form/mod.rs +++ b/core/lib/src/form/mod.rs @@ -45,7 +45,8 @@ //! * `map[k:1]=Bob` //! * `people[bob]nickname=Stan` //! -//! See [`FromForm`] for full details on push-parsing and complete examples. +//! See [`FromForm`] for full details on push-parsing and complete examples, and +//! [`Form`] for how to accept forms in a request handler. // ## Maps w/named Fields (`struct`) //