diff options
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | README.md | 34 | ||||
-rw-r--r-- | examples/csrf/src/main.rs | 9 | ||||
-rw-r--r-- | src/error.rs | 55 | ||||
-rw-r--r-- | src/lib.rs | 2 | ||||
-rw-r--r-- | src/request.rs | 8 |
6 files changed, 78 insertions, 32 deletions
@@ -1,6 +1,6 @@ [package] name = "sputnik" -version = "0.2.1" +version = "0.2.2" authors = ["Martin Fischer <martin@push-f.com>"] license = "MIT" description = "A lightweight layer on top of hyper to facilitate building web applications." @@ -10,7 +10,8 @@ Sputnik provides: (powered by the [cookie](https://crates.io/crates/cookie) crate) * parse query strings and HTML form data (powered by the [serde_urlencoded](https://crates.io/crates/serde_urlencoded) crate) -* an `Error` struct that makes it easy to centrally control the presentation of all error messages +* [an `Error` enum](#error-handling) that makes it easy to centrally control + the presentation of all error messages * cookie-based [CSRF](https://en.wikipedia.org/wiki/Cross-site_request_forgery) tokens * `Key`: a convenience wrapper around HMAC (stolen from the cookie crate, so that you don't have to use `CookieJar`s if you don't need them) @@ -25,6 +26,28 @@ Sputnik does **not**: * handle persistence: we recommend [diesel](https://diesel.rs/) * handle templating: we recommend [maud](https://maud.lambda.xyz/) +## Error handling + +Sputnik defines the following error types: + +```rust +pub struct SimpleError { + pub code: StatusCode, + pub message: String, +} + +pub enum Error { + Simple(SimpleError), + Response(hyper::Response<hyper::Body>), +} +``` + +Sputnik implements `Into<Error::Simple>` for all of its client error types +(e.g. deserialization errors), allowing you to easily customize the error +presentation. Sometimes however a `SimpleError` doesn't suffice, e.g. you +might want to redirect unauthorized users to your login page instead of +showing them an error, for such cases you can return an `Error::Response`. + ## CsrfToken example ```rust @@ -67,8 +90,13 @@ async fn service(req: hyper::Request<hyper::Body>) -> Result<hyper::Response<hyp let (mut parts, body) = sputnik::request::adapt(req); match route(&mut parts, body).await { Ok(res) => Ok(res.into()), - Err(err) => Ok(err.response_builder().body(err.message.into()).unwrap()) - // you can easily wrap or log errors here + Err(err) => match err { + Error::Simple(err) => { + Ok(err.response_builder().body(err.message.into()).unwrap()) + // you can easily wrap or log errors here + } + Error::Response(err) => Ok(err) + } } } diff --git a/examples/csrf/src/main.rs b/examples/csrf/src/main.rs index 16b31a1..497bd66 100644 --- a/examples/csrf/src/main.rs +++ b/examples/csrf/src/main.rs @@ -37,8 +37,13 @@ async fn service(req: hyper::Request<hyper::Body>) -> Result<hyper::Response<hyp let (mut parts, body) = sputnik::request::adapt(req); match route(&mut parts, body).await { Ok(res) => Ok(res.into()), - Err(err) => Ok(err.response_builder().body(err.message.into()).unwrap()) - // you can easily wrap or log errors here + Err(err) => match err { + Error::Simple(err) => { + Ok(err.response_builder().body(err.message.into()).unwrap()) + // you can easily wrap or log errors here + } + Error::Response(err) => Ok(err) + } } } diff --git a/src/error.rs b/src/error.rs index 5f35da2..fe998d0 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,55 +1,68 @@ -use std::fmt::Display; +//! Provides the [`crate::Error`] type. +use thiserror::Error; use hyper::StatusCode; /// Encapsulates a status code and an error message. -/// -/// All client errors in [`crate::request`] implement [`Into<Error>`]. -#[derive(Debug)] -pub struct Error { +#[derive(Error, Debug)] +#[error("SimpleError({code}, {message})")] +pub struct SimpleError { pub code: StatusCode, pub message: String, } -impl Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "error: {}", self.message) +impl SimpleError { + /// Returns an HTTP response builder with the status code set to `self.code`. + pub fn response_builder(&self) -> hyper::http::response::Builder { + hyper::Response::builder().status(self.code) } } -impl std::error::Error for Error { +/// Error type for request handlers. +/// +/// All client errors in [`crate::request`] implement [`Into<Error::Simple>`]. +#[derive(Error, Debug)] +pub enum Error { + #[error("SimpleError({}, {})", .0.code, .0.message)] + Simple(#[from] SimpleError), + + #[error("ResponseError({})", .0.status())] + Response(hyper::Response<hyper::Body>), } impl Error { - /// Returns an HTTP response builder with the status code set to `self.code`. - pub fn response_builder(&self) -> hyper::http::response::Builder { - hyper::Response::builder().status(self.code) - } + // some convenience methods - // some conventient constructors for common errors + pub fn simple(code: StatusCode, message: String) -> Self { + Error::Simple(SimpleError{code, message}) + } pub fn bad_request(message: String) -> Self { - Error{code: StatusCode::BAD_REQUEST, message} + Error::Simple(SimpleError{code: StatusCode::BAD_REQUEST, message}) } + pub fn not_found(message: String) -> Self { - Error{code: StatusCode::NOT_FOUND, message} + Error::Simple(SimpleError{code: StatusCode::NOT_FOUND, message}) } + pub fn unauthorized(message: String) -> Self { - Error{code: StatusCode::UNAUTHORIZED, message} + Error::Simple(SimpleError{code: StatusCode::UNAUTHORIZED, message}) } + pub fn internal(message: String) -> Self { - Error{code: StatusCode::INTERNAL_SERVER_ERROR, message} + Error::Simple(SimpleError{code: StatusCode::INTERNAL_SERVER_ERROR, message}) } + pub fn method_not_allowed(message: String) -> Self { - Error{code: StatusCode::METHOD_NOT_ALLOWED, message} + Error::Simple(SimpleError{code: StatusCode::METHOD_NOT_ALLOWED, message}) } } -macro_rules! impl_into_http_error { +macro_rules! impl_into_error_simple { ($type:ident, $status:expr) => { impl From<$type> for crate::error::Error { fn from(err: $type) -> Self { - Self{code: $status, message: format!("{}", err)} + Self::Simple(crate::error::SimpleError{code: $status, message: format!("{}", err)}) } } }; @@ -4,7 +4,7 @@ pub use error::Error; pub use mime; pub use httpdate; -#[macro_use] mod error; +#[macro_use] pub mod error; pub mod security; pub mod request; pub mod response; diff --git a/src/request.rs b/src/request.rs index 79ca2ae..c874ab5 100644 --- a/src/request.rs +++ b/src/request.rs @@ -169,12 +169,12 @@ pub mod error { #[derive(Error, Debug)] #[error("query deserialize error: {0}")] pub struct QueryError(pub serde_urlencoded::de::Error); - impl_into_http_error!(QueryError, StatusCode::BAD_REQUEST); + impl_into_error_simple!(QueryError, StatusCode::BAD_REQUEST); #[derive(Error, Debug)] #[error("failed to read body")] pub struct BodyError(pub hyper::Error); - impl_into_http_error!(BodyError, StatusCode::BAD_REQUEST); + impl_into_error_simple!(BodyError, StatusCode::BAD_REQUEST); #[derive(Error, Debug)] #[error("expected Content-Type {expected} but received {}", received.as_ref().unwrap_or(&"nothing".to_owned()))] @@ -194,7 +194,7 @@ pub mod error { #[error("form deserialize error: {0}")] Deserialize(#[from] serde_urlencoded::de::Error), } - impl_into_http_error!(FormError, StatusCode::BAD_REQUEST); + impl_into_error_simple!(FormError, StatusCode::BAD_REQUEST); #[derive(Error, Debug)] pub enum CsrfProtectedFormError { @@ -213,5 +213,5 @@ pub mod error { #[error("{0}")] Csrf(#[from] CsrfError), } - impl_into_http_error!(CsrfProtectedFormError, StatusCode::BAD_REQUEST); + impl_into_error_simple!(CsrfProtectedFormError, StatusCode::BAD_REQUEST); }
\ No newline at end of file |