diff --git a/README.md b/README.md index 0fd49595..3972b91b 100644 --- a/README.md +++ b/README.md @@ -1,283 +1,462 @@ # tower-web -This is *not* https://github.com/carllerche/tower-web even though the name is -the same. Its just a prototype of a minimal HTTP framework I've been toying -with. Will probably change the name to something else. +tower-web (name pending) is a tiny web application framework that focuses on +ergonimics and modularity. -# What is this? +### Goals -## Goals +- Ease of use. Build web apps in Rust should be as easy as `async fn +handle(Request) -> Response`. +- Solid foundation. tower-web is built on top of tower and makes it easy to +plug in any middleware from the [tower] and [tower-http] ecosystem. +- Focus on routing, extracing data from requests, and generating responses. +tower middleware can handle the rest. +- Macro free core. Macro frameworks have their place but tower-web focuses +on providing a core that is macro free. -- As easy to use as tide. I don't really consider warp easy to use due to type - tricks it uses. `fn route() -> impl Filter<...>` also isn't very ergonomic. - Just `async fn(Request) -> Response` would be nicer. -- Deep integration with Tower meaning you can - - Apply middleware to the entire application. - - Apply middleware to a single route. - - Apply middleware to subset of routes. -- Just focus on routing and generating responses. Tower can do the rest. - Want timeouts? Use `tower::timeout::Timeout`. Want logging? Use - `tower_http::trace::Trace`. -- Work with Tokio. tide is cool but requires async-std. -- Not macro based. Heavy macro based APIs can be very ergonomic but comes at a - complexity cost. Would like to see if I can design an API that is ergonomic - and doesn't require macros. +### Non-goals -## Non-goals +- Runtime independent. tower-web is designed to work with tokio and hyper +and focused on bringing a good to experience to that stack. +- Speed. tower-web is a of course a fast framework, and wont be the +bottleneck in your app, but the goal is not to top the benchmarks. -- Runtime independent. If becoming runtime independent isn't too much then fine - but explicitly designing for runtime independence isn't a goal. -- Speed. As long as things are reasonably fast that is fine. For example using - async-trait for ergonomics is fine even though it comes at a cost. +## Example -# Example usage - -NOTE: Error handling has changed quite a bit and these examples are slightly out -of date. See the examples for working examples. - -Defining a single route looks like this: +The "Hello, World!" of tower-web is: ```rust -let app = tower_web::app().at("/").get(root); +use tower_web::prelude::*; +use hyper::Server; +use std::net::SocketAddr; +use tower::make::Shared; -async fn root(req: Request) -> &'static str { - "Hello, World!" +#[tokio::main] +async fn main() { + // build our application with a single route + let app = route("/", get(handler)); + + // run it with hyper on localhost:3000 + let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + let server = Server::bind(&addr).serve(Shared::new(app)); + server.await.unwrap(); } -``` -Adding more routes follows the same pattern: - -```rust -let app = tower_web::app() - .at("/") - .get(root) - .at("/users") - .get(users_index) - .post(users_create); -``` - -Handler functions are just async functions like: - -```rust async fn handler(req: Request) -> &'static str { "Hello, World!" } ``` -They must take the request as the first argument but all arguments following -are called "extractors" and are used to extract data from the request (similar -to rocket but without macros): +## Routing + +Routing between handlers looks like this: ```rust -#[derive(Deserialize)] -struct UserPayload { - username: String, +use tower_web::prelude::*; + +let app = route("/", get(get_slash).post(post_slash)) + .route("/foo", get(get_foo)); + +async fn get_slash(req: Request) { + // `GET /` called } +async fn post_slash(req: Request) { + // `POST /` called +} + +async fn get_foo(req: Request) { + // `GET /foo` called +} +``` + +Routes can also be dynamic like `/users/:id`. See ["Extracting data from +requests"](#extracting-data-from-requests) for more details on that. + +## Responses + +Anything that implements [`IntoResponse`] can be returned from a handler: + +```rust +use tower_web::{body::Body, response::{Html, Json}, prelude::*}; +use http::{StatusCode, Response}; +use serde_json::{Value, json}; + +// We've already seen returning &'static str +async fn plain_text(req: Request) -> &'static str { + "foo" +} + +// String works too and will get a text/plain content-type +async fn plain_text_string(req: Request) -> String { + format!("Hi from {}", req.uri().path()) +} + +// Bytes will get a `application/octet-stream` content-type +async fn bytes(req: Request) -> Vec { + vec![1, 2, 3, 4] +} + +// `()` gives an empty response +async fn empty(req: Request) {} + +// `StatusCode` gives an empty response with that status code +async fn empty_with_status(req: Request) -> StatusCode { + StatusCode::NOT_FOUND +} + +// A tuple of `StatusCode` and something that implements `IntoResponse` can +// be used to override the status code +async fn with_status(req: Request) -> (StatusCode, &'static str) { + (StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong") +} + +// `Html` gives a content-type of `text/html` +async fn html(req: Request) -> Html<&'static str> { + Html("

Hello, World!

") +} + +// `Json` gives a content-type of `application/json` and works with my type +// that implements `serde::Serialize` +async fn json(req: Request) -> Json { + Json(json!({ "data": 42 })) +} + +// `Result` where `T` and `E` implement `IntoResponse` is useful for +// returning errors +async fn result(req: Request) -> Result<&'static str, StatusCode> { + Ok("all good") +} + +// `Response` gives full control +async fn response(req: Request) -> Response { + Response::builder().body(Body::empty()).unwrap() +} + +let app = route("/plain_text", get(plain_text)) + .route("/plain_text_string", get(plain_text_string)) + .route("/bytes", get(bytes)) + .route("/empty", get(empty)) + .route("/empty_with_status", get(empty_with_status)) + .route("/with_status", get(with_status)) + .route("/html", get(html)) + .route("/json", get(json)) + .route("/result", get(result)) + .route("/response", get(response)); +``` + +See the [`response`] module for more details. + +## Extracting data from requests + +A handler function must always take `Request` as its first argument +but any arguments following are called "extractors". Any type that +implements [`FromRequest`](crate::extract::FromRequest) can be used as an +extractor. + +[`extract::Json`] is an extractor that consumes the request body and +deserializes as as JSON into some target type: + +```rust +use tower_web::prelude::*; +use serde::Deserialize; + +let app = route("/users", post(create_user)); + +#[derive(Deserialize)] +struct CreateUser { + email: String, + password: String, +} + +async fn create_user(req: Request, payload: extract::Json) { + let payload: CreateUser = payload.0; + + // ... +} +``` + +[`extract::UrlParams`] can be used to extract params from a dynamic URL. It +is compatible with any type that implements [`std::str::FromStr`], such as +[`Uuid`]: + +```rust +use tower_web::prelude::*; +use uuid::Uuid; + +let app = route("/users/:id", post(create_user)); + +async fn create_user(req: Request, params: extract::UrlParams<(Uuid,)>) { + let (user_id,) = params.0; + + // ... +} +``` + +There is also [`UrlParamsMap`](extract::UrlParamsMap) which provide a map +like API for extracting URL params. + +You can also apply multiple extractors: + +```rust +use tower_web::prelude::*; +use uuid::Uuid; +use serde::Deserialize; + +let app = route("/users/:id/things", get(get_user_things)); + #[derive(Deserialize)] struct Pagination { page: usize, per_page: usize, } -async fn handler( +impl Default for Pagination { + fn default() -> Self { + Self { page: 1, per_page: 30 } + } +} + +async fn get_user_things( req: Request, - // deserialize response body with `serde_json` into a `UserPayload` - user: extract::Json, - // deserialize query string into a `Pagination` - pagination: extract::Query, -) -> &'static str { - let user: UserPayload = user.0; - let pagination: Pagination = pagination.0; + params: extract::UrlParams<(Uuid,)>, + pagination: Option>, +) { + let user_id: Uuid = (params.0).0; + let pagination: Pagination = pagination.unwrap_or_default().0; // ... } ``` -The inputs can also be optional: +See the [`extract`] module for more details. + +[`Uuid`]: https://docs.rs/uuid/latest/uuid/ + +## Applying middleware + +tower-web is designed to take full advantage of the tower and tower-http +ecosystem of middleware: + +### To individual handlers + +A middleware can be applied to a single handler like so: ```rust -async fn handler( - req: Request, - user: Option>, -) -> &'static str { - // ... -} +use tower_web::prelude::*; +use tower::limit::ConcurrencyLimitLayer; + +let app = route( + "/", + get(handler.layer(ConcurrencyLimitLayer::new(100))), +); + +async fn handler(req: Request) {} ``` -You can also get the raw response body: +### To groups of routes + +Middleware can also be applied to a group of routes like so: ```rust -async fn handler( - req: Request, - // buffer the whole request body - body: Bytes, -) -> &'static str { - // ... -} +use tower_web::prelude::*; +use tower::limit::ConcurrencyLimitLayer; + +let app = route("/", get(get_slash)) + .route("/foo", post(post_foo)) + .layer(ConcurrencyLimitLayer::new(100)); + +async fn get_slash(req: Request) {} + +async fn post_foo(req: Request) {} ``` -Or limit the body size: +### Error handling + +tower-web requires all errors to be handled. That is done by using +[`std::convert::Infallible`] as the error type in all its [`Service`] +implementations. + +For handlers created from async functions this is works automatically since +handlers must return something that implements [`IntoResponse`], even if its +a `Result`. + +However middleware might add new failure cases that has to be handled. For +that tower-web provides a `handle_error` combinator: ```rust -async fn handler( - req: Request, - // max body size in bytes - body: extract::BytesMaxLength<1024>, -) -> &'static str { - // ... -} +use tower_web::prelude::*; +use tower::{ + BoxError, timeout::{TimeoutLayer, error::Elapsed}, +}; +use std::{borrow::Cow, time::Duration}; +use http::StatusCode; + +let app = route( + "/", + get(handle + .layer(TimeoutLayer::new(Duration::from_secs(30))) + // `Timeout` uses `BoxError` as the error type + .handle_error(|error: BoxError| { + // Check if the actual error type is `Elapsed` which + // `Timeout` returns + if error.is::() { + return (StatusCode::REQUEST_TIMEOUT, "Request took too long".into()); + } + + // If we encounter some error we don't handle return a generic + // error + return ( + StatusCode::INTERNAL_SERVER_ERROR, + // `Cow` lets us return either `&str` or `String` + Cow::from(format!("Unhandled internal error: {}", error)), + ); + })), +); + +async fn handle(req: Request) {} ``` -Params from dynamic routes like `GET /users/:id` can be extracted like so +The closure passed to `handle_error` must return something that implements +`IntoResponse`. + +`handle_error` is also available on a group of routes with middleware +applied: ```rust -async fn handle( - req: Request, - // get a map of key value pairs - map: extract::UrlParamsMap, -) -> &'static str { - let raw_id: Option<&str> = map.get("id"); - let parsed_id: Option = map.get_typed::("id"); +use tower_web::prelude::*; +use tower::{ + BoxError, timeout::{TimeoutLayer, error::Elapsed}, +}; +use std::{borrow::Cow, time::Duration}; +use http::StatusCode; - // ... -} +let app = route("/", get(handle)) + .layer(TimeoutLayer::new(Duration::from_secs(30))) + .handle_error(|error: BoxError| { + // ... + }); -async fn handle( - req: Request, - // or get a tuple with each param - params: extract::UrlParams<(i32, String)>, -) -> &'static str { - let (id, name) = params.0; - - // ... -} +async fn handle(req: Request) {} ``` -If you wanna go all out you can even deconstruct the extractor directly in the -function signature: +### Applying multiple middleware + +[`tower::ServiceBuilder`] can be used to combine multiple middleware: ```rust -async fn handle( - req: Request, - UrlParams((id, name)): UrlParams<(i32, String)>, -) -> &'static str { - // ... -} -``` +use tower_web::prelude::*; +use tower::{ + ServiceBuilder, BoxError, + load_shed::error::Overloaded, + timeout::error::Elapsed, +}; +use tower_http::compression::CompressionLayer; +use std::{borrow::Cow, time::Duration}; +use http::StatusCode; -Anything that implements `FromRequest` can work as an extractor where -`FromRequest` is an async trait: - -```rust -#[async_trait] -pub trait FromRequest: Sized { - type Rejection: IntoResponse; - - async fn from_request(req: &mut Request) -> Result; -} -``` - -This "extractor" pattern is inspired by Bevy's ECS. The idea is that it should -be easy to pick apart the request without having to repeat yourself a lot or use -macros. - -The return type must implement `IntoResponse`: - -```rust -async fn empty_response(req: Request) { - // ... -} - -// gets `content-type: text/plain` -async fn string_response(req: Request) -> String { - // ... -} - -// gets `content-type: appliation/json`. `Json` can contain any `T: Serialize` -async fn json_response(req: Request) -> response::Json { - // ... -} - -// gets `content-type: text/html`. `Html` can contain any `T: Into` -async fn html_response(req: Request) -> response::Html { - // ... -} - -// or for full control -async fn response(req: Request) -> Response { - // ... -} - -// Result is supported if each type implements `IntoResponse` -async fn response(req: Request) -> Result, StatusCode> { - // ... -} -``` - -This makes error handling quite simple. Basically handlers are not allowed to -fail and must always produce a response. This also means users are in charge of -how their errors are mapped to responses rather than a framework providing some -opaque catch all error type. - -You can also apply Tower middleware to single routes: - -```rust -let app = tower_web::app() - .at("/") - .get(send_some_large_file.layer(CompressionLayer::new())) -``` - -Or to the whole app: - -```rust -let service = tower_web::app() - .at("/") - .get(root) - .into_service() - -let app = ServiceBuilder::new() +let middleware_stack = ServiceBuilder::new() + // Return an error after 30 seconds .timeout(Duration::from_secs(30)) - .layer(TraceLayer::new_for_http()) + // Shed load if we're receiving too many requests + .load_shed() + // Process at most 100 requests concurrently + .concurrency_limit(100) + // Compress response bodies .layer(CompressionLayer::new()) - .service(app); + .into_inner(); + +let app = route("/", get(|_: Request| async { /* ... */ })) + .layer(middleware_stack) + .handle_error(|error: BoxError| { + if error.is::() { + return ( + StatusCode::SERVICE_UNAVAILABLE, + "Try again later".into(), + ); + } + + if error.is::() { + return ( + StatusCode::REQUEST_TIMEOUT, + "Request took too long".into(), + ); + }; + + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Cow::from(format!("Unhandled internal error: {}", error)), + ); + }); ``` -And of course run it with Hyper: +## Sharing state with handlers + +It is common to share some state between handlers for example to share a +pool of database connections or clients to other services. That can be done +using the [`AddExtension`] middleware (applied with [`AddExtensionLayer`]) +and the [`extract::Extension`] extractor: ```rust -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); +use tower_web::{AddExtensionLayer, prelude::*}; +use std::sync::Arc; - // build our application with some routes - let app = tower_web::app() - .at("/") - .get(handler) - // convert it into a `Service` - .into_service(); +struct State { + // ... +} - // add some middleware - let app = ServiceBuilder::new() - .layer(TraceLayer::new_for_http()) - .service(app); +let shared_state = Arc::new(State { /* ... */ }); - // run it - let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); - tracing::debug!("listening on {}", addr); - let server = Server::bind(&addr).serve(Shared::new(app)); - server.await.unwrap(); +let app = route("/", get(handler)).layer(AddExtensionLayer::new(shared_state)); + +async fn handler( + req: Request, + state: extract::Extension>, +) { + let state: Arc = state.0; + + // ... } ``` -See the examples directory for more examples. +## Routing to any [`Service`] -# TODO +tower-web also supports routing to general [`Service`]s: -- `RouteBuilder` should have an `async fn serve(self) -> Result<(), - hyper::Error>` for users who just wanna create a hyper server and not care - about the lower level details. Should be gated by a `hyper` feature. -- Each new route makes a new allocation for the response body, since `Or` needs - to unify the response body types. Would be nice to find a way to avoid that. -- It should be possible to package some routes together and apply a tower - middleware to that collection and then merge those routes into the app. +```rust +use tower_web::{ + service, prelude::*, + // `ServiceExt` adds `handle_error` to any `Service` + ServiceExt, +}; +use tower_http::services::ServeFile; +use http::Response; +use std::convert::Infallible; +use tower::{service_fn, BoxError}; + +let app = route( + // Any request to `/` goes to a service + "/", + service_fn(|_: Request| async { + let res = Response::new(Body::from("Hi from `GET /`")); + Ok::<_, Infallible>(res) + }) +).route( + // GET `/static/Cargo.toml` goes to a service from tower-http + "/static/Cargo.toml", + service::get( + ServeFile::new("Cargo.toml") + // Errors must be handled + .handle_error(|error: std::io::Error| { /* ... */ }) + ) +); +``` + +See the [`service`] module for more details. + +## Nesting applications + +TODO + +[tower]: https://crates.io/crates/tower +[tower-http]: https://crates.io/crates/tower-http