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
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<Body>) -> &'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<Body>) -> &'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<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)]
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<Body>,
// deserialize response body with `serde_json` into a `UserPayload`
user: extract::Json<UserPayload>,
// deserialize query string into a `Pagination`
pagination: extract::Query<Pagination>,
) -> &'static str {
let user: UserPayload = user.0;
let pagination: Pagination = pagination.0;
params: extract::UrlParams<(Uuid,)>,
pagination: Option<extract::Query<Pagination>>,
) {
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<Body>,
user: Option<extract::Json<UserPayload>>,
) -> &'static str {
// ...
}
use tower_web::prelude::*;
use tower::limit::ConcurrencyLimitLayer;
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
async fn handler(
req: Request<Body>,
// 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<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
async fn handler(
req: Request<Body>,
// 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::<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
async fn handle(
req: Request<Body>,
// get a map of key value pairs
map: extract::UrlParamsMap,
) -> &'static str {
let raw_id: Option<&str> = map.get("id");
let parsed_id: Option<i32> = map.get_typed::<i32>("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<Body>,
// or get a tuple with each param
params: extract::UrlParams<(i32, String)>,
) -> &'static str {
let (id, name) = params.0;
// ...
}
async fn handle(req: Request<Body>) {}
```
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<Body>,
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<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()
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<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
#[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<Body>,
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<(),
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<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