Sputnik
This library extends the types from the http crate:
- extends http::request::Partswith query parameter deserialization & cookie parsing
- extends http::response::Builderwith methods to set cookies and content-types
If you use Hyper and want to deserialize request bodies with Serde you can enable the following feature flags:
- hyper_bodyprovides a trait to extend- hyper::Bodywith an- into_formmethod for parsing data submitted from HTML forms.
- hyper_body_jsonadditionaly provides an- into_jsonmethod
Sputnik does not handle routing because even complex routing can be quite
easily implemented with nested match blocks. If you want a more high-level
router, you can check out the router crates.
Sputnik encourages you to create your own error enum and implement From
conversions for every error type, which you want to short-circuit with the ?
operator. This can be easily done with thiserror
because Sputnik restricts its error types to the 'static lifetime.
Security Considerations
Protect your application against CSRF
by setting SameSite to Lax or Strict for your cookies and checking that the Origin
header matches your domain name (especially if you have unauthenticated POST endpoints).
Hyper Example
use hyper::http::request::Parts;
use hyper::http::response::Builder;
use hyper::service::{make_service_fn, service_fn};
use hyper::{Body, Method, Server, StatusCode};
use serde::Deserialize;
use sputnik::hyper_body::{FormError, SputnikBody};
use sputnik::{html_escape, mime, request::SputnikParts, response::SputnikBuilder};
use std::convert::Infallible;
type Response = hyper::Response<Body>;
#[derive(thiserror::Error, Debug)]
enum Error {
    #[error("page not found")]
    NotFound(String),
    #[error("{0}")]
    FormError(#[from] FormError),
}
fn render_error(err: Error) -> (StatusCode, String) {
    match err {
        Error::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
        Error::FormError(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") => Ok(get_form(req)),
        (&Method::POST, "/form") => post_form(req, body).await,
        _ => return Err(Error::NotFound("page not found".to_owned())),
    }
}
fn get_form(_req: &mut Parts) -> Response {
    Builder::new()
        .content_type(mime::TEXT_HTML)
        .body("<form method=post><input name=text> <button>Submit</button></form>".into())
        .unwrap()
}
#[derive(Deserialize)]
struct FormData {
    text: String,
}
async fn post_form(_req: &mut Parts, body: Body) -> Result<Response, Error> {
    let FormData { text } = body.into_form().await?;
    Ok(Builder::new()
        .content_type(mime::TEXT_HTML)
        .body(format!("hello <em>{}</em>", html_escape(text)).into())
        .unwrap())
}
async fn service(
    req: hyper::Request<hyper::Body>,
) -> Result<hyper::Response<hyper::Body>, Infallible> {
    let (mut parts, body) = req.into_parts();
    match route(&mut parts, body).await {
        Ok(mut res) => {
            for (k, v) in parts.response_headers().iter() {
                res.headers_mut().append(k, v.clone());
            }
            Ok(res)
        }
        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())
        }
    }
}
#[tokio::main]
async fn main() {
    let service = make_service_fn(move |_| async move {
        Ok::<_, hyper::Error>(service_fn(move |req| service(req)))
    });
    let addr = ([127, 0, 0, 1], 8000).into();
    let server = Server::bind(&addr).serve(service);
    println!("Listening on http://{}", addr);
    server.await;
}
