aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMartin Fischer <martin@push-f.com>2021-01-22 21:34:35 +0100
committerMartin Fischer <martin@push-f.com>2021-01-22 21:36:53 +0100
commit4ba2d050bdf1a3c0070f3aa2331c82745611af1f (patch)
tree710b2d96dba397da5d5888f5099402f6271dc9fd
parentb886de1afc0b90d7ca27db9d5c7dabddbe3d7ee0 (diff)
completely ditch Sputnik's general Error type
Users also want to short-circuit error types from other crates but they cannot define a From conversion between two foreign types. Sputnik's error type also didn't allow for proper error logging. bump version to 0.2.3
-rw-r--r--Cargo.toml2
-rw-r--r--README.md75
-rw-r--r--examples/csrf/Cargo.toml3
-rw-r--r--examples/csrf/src/main.rs32
-rw-r--r--src/error.rs69
-rw-r--r--src/lib.rs2
-rw-r--r--src/request.rs24
7 files changed, 72 insertions, 135 deletions
diff --git a/Cargo.toml b/Cargo.toml
index fe55027..c58525e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "sputnik"
-version = "0.2.2"
+version = "0.2.3"
authors = ["Martin Fischer <martin@push-f.com>"]
license = "MIT"
description = "A lightweight layer on top of hyper to facilitate building web applications."
diff --git a/README.md b/README.md
index 2b006e6..b2fe919 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,9 @@
# Sputnik
-A lightweight layer on top of [Hyper](https://hyper.rs/) to facilitate
-building web applications.
+A microframework based on [Hyper](https://hyper.rs/) that forces you to:
+
+* make error handling explicit (no possible failures hidden behind macros)
+* implement your own error type ([because you need to anyway](#error-handling))
Sputnik provides:
@@ -10,8 +12,6 @@ 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` 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)
@@ -28,41 +28,48 @@ Sputnik does **not**:
## Error handling
-Sputnik defines the following error types:
-
-```rust
-pub struct SimpleError {
- pub code: StatusCode,
- pub message: String,
-}
+Rust provides convenient short-circuiting with the `?` operator, which
+converts errors with `From::from()`. Since you probably want to short-circuit
+errors from other crates (e.g. database errors), a web framework cannot
+provide you an error type since Rust disallows you from defining a `From`
+conversion between two foreign types.
-pub enum Error {
- Simple(SimpleError),
- Response(hyper::Response<hyper::Body>),
-}
-```
+This does imply that you need to define your own error type, allowing you to
+implement a `From` conversion for every error type you want to short-circuit
+with `?`. Fortunately the [thiserror](https://crates.io/crates/thiserror)
+crate makes defining custom errors and `From` implementations trivial.
-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
+## Example
```rust
use std::convert::Infallible;
use hyper::service::{service_fn, make_service_fn};
-use hyper::{Method, Server};
+use hyper::{Method, Server, StatusCode};
use serde::Deserialize;
use sputnik::security::CsrfToken;
-use sputnik::{Error, request::{Parts, Body}, response::Response};
+use sputnik::{request::{Parts, Body}, response::Response};
+use sputnik::request::error::*;
+
+#[derive(thiserror::Error, Debug)]
+enum Error {
+ #[error("page not found")]
+ NotFound(String),
+ #[error("{0}")]
+ CsrfError(#[from] CsrfProtectedFormError)
+}
+
+fn render_error(err: Error) -> (StatusCode, String) {
+ match err {
+ Error::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
+ Error::CsrfError(err) => (StatusCode::BAD_REQUEST, err.to_string()),
+ }
+}
async fn route(req: &mut Parts, body: Body) -> Result<Response,Error> {
match (req.method(), req.uri().path()) {
(&Method::GET, "/form") => get_form(req).await,
(&Method::POST, "/form") => post_form(req, body).await,
- _ => return Err(Error::not_found("page not found".to_owned()))
+ _ => return Err(Error::NotFound("page not found".to_owned()))
}
}
@@ -90,12 +97,10 @@ 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) => 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)
+ Err(err) => {
+ let (code, message) = render_error(err);
+ // you can easily wrap or log errors here
+ Ok(hyper::Response::builder().status(code).body(message.into()).unwrap())
}
}
}
@@ -138,9 +143,9 @@ This session id cookie can then be retrieved and verified as follows:
```rust
let userid = req.cookies().get("userid")
- .ok_or_else(|| Error::unauthorized("expected userid cookie".to_owned()))
- .and_then(|cookie| key.verify(cookie.value()).map_err(Error::unauthorized))
- .and_then(|value| decode_expiring_claim(value).map_err(|e| Error::unauthorized(format!("failed to decode userid cookie: {}", e))))?;
+ .ok_or_else(|| "expected userid cookie".to_owned())
+ .and_then(|cookie| key.verify(cookie.value())
+ .and_then(|value| decode_expiring_claim(value).map_err(|e| format!("failed to decode userid cookie: {}", e)));
```
Tip: If you want to store multiple claims in the cookie, you can
diff --git a/examples/csrf/Cargo.toml b/examples/csrf/Cargo.toml
index 1f0066b..b6768ed 100644
--- a/examples/csrf/Cargo.toml
+++ b/examples/csrf/Cargo.toml
@@ -11,4 +11,5 @@ publish = false
hyper = "0.13"
sputnik = {path = "../../"}
serde = { version = "1.0", features = ["derive"] }
-tokio = { version = "0.2", features = ["full"] } \ No newline at end of file
+tokio = { version = "0.2", features = ["full"] }
+thiserror = "1.0" \ No newline at end of file
diff --git a/examples/csrf/src/main.rs b/examples/csrf/src/main.rs
index 497bd66..915d063 100644
--- a/examples/csrf/src/main.rs
+++ b/examples/csrf/src/main.rs
@@ -1,15 +1,31 @@
use std::convert::Infallible;
use hyper::service::{service_fn, make_service_fn};
-use hyper::{Method, Server};
+use hyper::{Method, Server, StatusCode};
use serde::Deserialize;
use sputnik::security::CsrfToken;
-use sputnik::{Error, request::{Parts, Body}, response::Response};
+use sputnik::{request::{Parts, Body}, response::Response};
+use sputnik::request::error::*;
+
+#[derive(thiserror::Error, Debug)]
+enum Error {
+ #[error("page not found")]
+ NotFound(String),
+ #[error("{0}")]
+ CsrfError(#[from] CsrfProtectedFormError)
+}
+
+fn render_error(err: Error) -> (StatusCode, String) {
+ match err {
+ Error::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
+ Error::CsrfError(err) => (StatusCode::BAD_REQUEST, err.to_string()),
+ }
+}
async fn route(req: &mut Parts, body: Body) -> Result<Response,Error> {
match (req.method(), req.uri().path()) {
(&Method::GET, "/form") => get_form(req).await,
(&Method::POST, "/form") => post_form(req, body).await,
- _ => return Err(Error::not_found("page not found".to_owned()))
+ _ => return Err(Error::NotFound("page not found".to_owned()))
}
}
@@ -37,12 +53,10 @@ 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) => 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)
+ Err(err) => {
+ let (code, message) = render_error(err);
+ // you can easily wrap or log errors here
+ Ok(hyper::Response::builder().status(code).body(message.into()).unwrap())
}
}
}
diff --git a/src/error.rs b/src/error.rs
deleted file mode 100644
index fe998d0..0000000
--- a/src/error.rs
+++ /dev/null
@@ -1,69 +0,0 @@
-//! Provides the [`crate::Error`] type.
-
-use thiserror::Error;
-use hyper::StatusCode;
-
-/// Encapsulates a status code and an error message.
-#[derive(Error, Debug)]
-#[error("SimpleError({code}, {message})")]
-pub struct SimpleError {
- pub code: StatusCode,
- pub message: String,
-}
-
-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)
- }
-}
-
-/// 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 {
- // some convenience methods
-
- pub fn simple(code: StatusCode, message: String) -> Self {
- Error::Simple(SimpleError{code, message})
- }
-
- pub fn bad_request(message: String) -> Self {
- Error::Simple(SimpleError{code: StatusCode::BAD_REQUEST, message})
- }
-
- pub fn not_found(message: String) -> Self {
- Error::Simple(SimpleError{code: StatusCode::NOT_FOUND, message})
- }
-
- pub fn unauthorized(message: String) -> Self {
- Error::Simple(SimpleError{code: StatusCode::UNAUTHORIZED, message})
- }
-
- pub fn internal(message: String) -> Self {
- Error::Simple(SimpleError{code: StatusCode::INTERNAL_SERVER_ERROR, message})
- }
-
- pub fn method_not_allowed(message: String) -> Self {
- Error::Simple(SimpleError{code: StatusCode::METHOD_NOT_ALLOWED, message})
- }
-}
-
-macro_rules! impl_into_error_simple {
- ($type:ident, $status:expr) => {
- impl From<$type> for crate::error::Error {
- fn from(err: $type) -> Self {
- Self::Simple(crate::error::SimpleError{code: $status, message: format!("{}", err)})
- }
- }
- };
-} \ No newline at end of file
diff --git a/src/lib.rs b/src/lib.rs
index 391eb2c..0841e0b 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,10 +1,8 @@
//! A lightweight layer on top of [Hyper](https://hyper.rs/)
//! to facilitate building web applications.
-pub use error::Error;
pub use mime;
pub use httpdate;
-#[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 c874ab5..953e7ec 100644
--- a/src/request.rs
+++ b/src/request.rs
@@ -93,13 +93,13 @@ impl Body {
///
/// ```
/// use hyper::{Response};
- /// use sputnik::{request::Body, Error};
+ /// use sputnik::request::{Body, error::FormError};
/// use serde::Deserialize;
///
/// #[derive(Deserialize)]
/// struct Message {text: String, year: i64}
///
- /// async fn greet(body: Body) -> Result<Response<hyper::Body>, Error> {
+ /// async fn greet(body: Body) -> Result<Response<hyper::Body>, FormError> {
/// let msg: Message = body.into_form().await?;
/// Ok(Response::new(format!("hello {}", msg.text).into()))
/// }
@@ -120,25 +120,18 @@ impl Body {
///
/// ```
/// use hyper::{Method};
- /// use sputnik::{request::{Parts, Body}, response::Response, Error};
+ /// use sputnik::{request::{Parts, Body, error::CsrfProtectedFormError}, response::Response};
/// use sputnik::security::CsrfToken;
/// use serde::Deserialize;
///
/// #[derive(Deserialize)]
/// struct Message {text: String}
///
- /// async fn greet(req: &mut Parts, body: Body) -> Result<Response, Error> {
+ /// async fn greet(req: &mut Parts, body: Body) -> Result<Response, CsrfProtectedFormError> {
/// let mut response = Response::new();
/// let csrf_token = CsrfToken::from_parts(req, &mut response);
- /// *response.body() = match (req.method()) {
- /// &Method::GET => format!("<form method=post>
- /// <input name=text>{}<button>Submit</button></form>", csrf_token.html_input()).into(),
- /// &Method::POST => {
- /// let msg: Message = body.into_form_csrf(&csrf_token).await?;
- /// format!("hello {}", msg.text).into()
- /// },
- /// _ => return Err(Error::method_not_allowed("only GET and POST allowed".to_owned())),
- /// };
+ /// let msg: Message = body.into_form_csrf(&csrf_token).await?;
+ /// *response.body() = format!("hello {}", msg.text).into();
/// Ok(response)
/// }
/// ```
@@ -163,18 +156,15 @@ impl Body {
pub mod error {
use mime::Mime;
use thiserror::Error;
- use hyper::StatusCode;
use crate::security::CsrfError;
#[derive(Error, Debug)]
#[error("query deserialize error: {0}")]
pub struct QueryError(pub serde_urlencoded::de::Error);
- impl_into_error_simple!(QueryError, StatusCode::BAD_REQUEST);
#[derive(Error, Debug)]
#[error("failed to read body")]
pub struct BodyError(pub hyper::Error);
- 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 +184,6 @@ pub mod error {
#[error("form deserialize error: {0}")]
Deserialize(#[from] serde_urlencoded::de::Error),
}
- impl_into_error_simple!(FormError, StatusCode::BAD_REQUEST);
#[derive(Error, Debug)]
pub enum CsrfProtectedFormError {
@@ -213,5 +202,4 @@ pub mod error {
#[error("{0}")]
Csrf(#[from] CsrfError),
}
- impl_into_error_simple!(CsrfProtectedFormError, StatusCode::BAD_REQUEST);
} \ No newline at end of file