Change readme

This commit is contained in:
David Pedersen 2021-06-06 15:20:27 +02:00
parent a005427d40
commit 35ab973acb
1 changed files with 393 additions and 214 deletions

607
README.md
View File

@ -1,283 +1,462 @@
# tower-web # tower-web
This is *not* https://github.com/carllerche/tower-web even though the name is tower-web (name pending) is a tiny web application framework that focuses on
the same. Its just a prototype of a minimal HTTP framework I've been toying ergonimics and modularity.
with. Will probably change the name to something else.
# 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 ### Non-goals
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 - 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 ## Example
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 usage The "Hello, World!" of tower-web is:
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:
```rust ```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<Body>) -> &'static str { #[tokio::main]
"Hello, World!" 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<Body>) -> &'static str { async fn handler(req: Request<Body>) -> &'static str {
"Hello, World!" "Hello, World!"
} }
``` ```
They must take the request as the first argument but all arguments following ## Routing
are called "extractors" and are used to extract data from the request (similar
to rocket but without macros): Routing between handlers looks like this:
```rust ```rust
#[derive(Deserialize)] use tower_web::prelude::*;
struct UserPayload {
username: String, let app = route("/", get(get_slash).post(post_slash))
.route("/foo", get(get_foo));
async fn get_slash(req: Request<Body>) {
// `GET /` called
} }
async fn post_slash(req: Request<Body>) {
// `POST /` called
}
async fn get_foo(req: Request<Body>) {
// `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<Body>) -> &'static str {
"foo"
}
// String works too and will get a text/plain content-type
async fn plain_text_string(req: Request<Body>) -> String {
format!("Hi from {}", req.uri().path())
}
// Bytes will get a `application/octet-stream` content-type
async fn bytes(req: Request<Body>) -> Vec<u8> {
vec![1, 2, 3, 4]
}
// `()` gives an empty response
async fn empty(req: Request<Body>) {}
// `StatusCode` gives an empty response with that status code
async fn empty_with_status(req: Request<Body>) -> 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<Body>) -> (StatusCode, &'static str) {
(StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong")
}
// `Html` gives a content-type of `text/html`
async fn html(req: Request<Body>) -> Html<&'static str> {
Html("<h1>Hello, World!</h1>")
}
// `Json` gives a content-type of `application/json` and works with my type
// that implements `serde::Serialize`
async fn json(req: Request<Body>) -> Json<Value> {
Json(json!({ "data": 42 }))
}
// `Result<T, E>` where `T` and `E` implement `IntoResponse` is useful for
// returning errors
async fn result(req: Request<Body>) -> Result<&'static str, StatusCode> {
Ok("all good")
}
// `Response` gives full control
async fn response(req: Request<Body>) -> Response<Body> {
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<Body>` 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<Body>, payload: extract::Json<CreateUser>) {
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<Body>, 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)] #[derive(Deserialize)]
struct Pagination { struct Pagination {
page: usize, page: usize,
per_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<Body>, req: Request<Body>,
// deserialize response body with `serde_json` into a `UserPayload` params: extract::UrlParams<(Uuid,)>,
user: extract::Json<UserPayload>, pagination: Option<extract::Query<Pagination>>,
// deserialize query string into a `Pagination` ) {
pagination: extract::Query<Pagination>, let user_id: Uuid = (params.0).0;
) -> &'static str { let pagination: Pagination = pagination.unwrap_or_default().0;
let user: UserPayload = user.0;
let pagination: Pagination = pagination.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 ```rust
async fn handler( use tower_web::prelude::*;
req: Request<Body>, use tower::limit::ConcurrencyLimitLayer;
user: Option<extract::Json<UserPayload>>,
) -> &'static str { let app = route(
// ... "/",
} get(handler.layer(ConcurrencyLimitLayer::new(100))),
);
async fn handler(req: Request<Body>) {}
``` ```
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 ```rust
async fn handler( use tower_web::prelude::*;
req: Request<Body>, use tower::limit::ConcurrencyLimitLayer;
// buffer the whole request body
body: Bytes, let app = route("/", get(get_slash))
) -> &'static str { .route("/foo", post(post_foo))
// ... .layer(ConcurrencyLimitLayer::new(100));
}
async fn get_slash(req: Request<Body>) {}
async fn post_foo(req: Request<Body>) {}
``` ```
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 ```rust
async fn handler( use tower_web::prelude::*;
req: Request<Body>, use tower::{
// max body size in bytes BoxError, timeout::{TimeoutLayer, error::Elapsed},
body: extract::BytesMaxLength<1024>, };
) -> &'static str { 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::<Elapsed>() {
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<Body>) {}
``` ```
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 ```rust
async fn handle( use tower_web::prelude::*;
req: Request<Body>, use tower::{
// get a map of key value pairs BoxError, timeout::{TimeoutLayer, error::Elapsed},
map: extract::UrlParamsMap, };
) -> &'static str { use std::{borrow::Cow, time::Duration};
let raw_id: Option<&str> = map.get("id"); use http::StatusCode;
let parsed_id: Option<i32> = map.get_typed::<i32>("id");
// ... let app = route("/", get(handle))
} .layer(TimeoutLayer::new(Duration::from_secs(30)))
.handle_error(|error: BoxError| {
// ...
});
async fn handle( async fn handle(req: Request<Body>) {}
req: Request<Body>,
// or get a tuple with each param
params: extract::UrlParams<(i32, String)>,
) -> &'static str {
let (id, name) = params.0;
// ...
}
``` ```
If you wanna go all out you can even deconstruct the extractor directly in the ### Applying multiple middleware
function signature:
[`tower::ServiceBuilder`] can be used to combine multiple middleware:
```rust ```rust
async fn handle( use tower_web::prelude::*;
req: Request<Body>, use tower::{
UrlParams((id, name)): UrlParams<(i32, String)>, ServiceBuilder, BoxError,
) -> &'static str { 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 let middleware_stack = ServiceBuilder::new()
`FromRequest` is an async trait: // Return an error after 30 seconds
```rust
#[async_trait]
pub trait FromRequest: Sized {
type Rejection: IntoResponse<B>;
async fn from_request(req: &mut Request<Body>) -> Result<Self, Self::Rejection>;
}
```
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<Body>) {
// ...
}
// gets `content-type: text/plain`
async fn string_response(req: Request<Body>) -> String {
// ...
}
// gets `content-type: appliation/json`. `Json` can contain any `T: Serialize`
async fn json_response(req: Request<Body>) -> response::Json<User> {
// ...
}
// gets `content-type: text/html`. `Html` can contain any `T: Into<Bytes>`
async fn html_response(req: Request<Body>) -> response::Html<String> {
// ...
}
// or for full control
async fn response(req: Request<Body>) -> Response<Body> {
// ...
}
// Result is supported if each type implements `IntoResponse`
async fn response(req: Request<Body>) -> Result<Html<String>, 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()
.timeout(Duration::from_secs(30)) .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()) .layer(CompressionLayer::new())
.service(app); .into_inner();
let app = route("/", get(|_: Request<Body>| async { /* ... */ }))
.layer(middleware_stack)
.handle_error(|error: BoxError| {
if error.is::<Overloaded>() {
return (
StatusCode::SERVICE_UNAVAILABLE,
"Try again later".into(),
);
}
if error.is::<Elapsed>() {
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 ```rust
#[tokio::main] use tower_web::{AddExtensionLayer, prelude::*};
async fn main() { use std::sync::Arc;
tracing_subscriber::fmt::init();
// build our application with some routes struct State {
let app = tower_web::app() // ...
.at("/") }
.get(handler)
// convert it into a `Service`
.into_service();
// add some middleware let shared_state = Arc::new(State { /* ... */ });
let app = ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
.service(app);
// run it let app = route("/", get(handler)).layer(AddExtensionLayer::new(shared_state));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
tracing::debug!("listening on {}", addr); async fn handler(
let server = Server::bind(&addr).serve(Shared::new(app)); req: Request<Body>,
server.await.unwrap(); state: extract::Extension<Arc<State>>,
) {
let state: Arc<State> = 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<(), ```rust
hyper::Error>` for users who just wanna create a hyper server and not care use tower_web::{
about the lower level details. Should be gated by a `hyper` feature. service, prelude::*,
- Each new route makes a new allocation for the response body, since `Or` needs // `ServiceExt` adds `handle_error` to any `Service`
to unify the response body types. Would be nice to find a way to avoid that. ServiceExt,
- 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. 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<Body>| 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