diff options
Diffstat (limited to 'README.md')
-rw-r--r-- | README.md | 75 |
1 files changed, 40 insertions, 35 deletions
@@ -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 |