aboutsummaryrefslogtreecommitdiff
path: root/README.md
diff options
context:
space:
mode:
Diffstat (limited to 'README.md')
-rw-r--r--README.md75
1 files changed, 40 insertions, 35 deletions
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