More work

This commit is contained in:
David Pedersen 2021-05-31 16:28:26 +02:00
parent 867dd8012c
commit f6b1a6f435
8 changed files with 600 additions and 27 deletions

View file

@ -16,7 +16,7 @@ serde = "1.0"
serde_json = "1.0"
serde_urlencoded = "0.7"
thiserror = "1.0"
tower = { version = "0.4", features = ["util"] }
tower = { version = "0.4", features = ["util", "buffer"] }
[dev-dependencies]
hyper = { version = "0.14", features = ["full"] }

217
README.md Normal file
View file

@ -0,0 +1,217 @@
# 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.
# What is this?
## Goals
- 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) -> Result<Response, Error>` 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. 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 usage
Defining a single route looks like this:
```rust
let app = tower_web::app().at("/").get(root);
async fn root(req: Request<Body>) -> Result<&'static str, Error> {
Ok("Hello, World!")
}
```
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>) -> Result<&'static str, Error> {
Ok("Hello, World!")
}
```
They most 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 no macros):
```rust
#[derive(Deserialize)]
struct UserPayload {
username: String,
}
#[derive(Deserialize)]
struct Pagination {
page: usize,
per_page: usize,
}
async fn handler(
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>,
) -> Result<&'static str, Error> {
let user: UserPayload = user.into_inner();
let pagination: Pagination = pagination.into_inner();
// ...
}
```
You can also get the raw response body:
```rust
async fn handler(
req: Request<Body>,
// buffer the whole request body
body: Bytes,
) -> Result<&'static str, Error> {
// ...
}
```
Or limit the body size:
```rust
async fn handler(
req: Request<Body>,
// max body size in bytes
body: extract::BytesMaxLength<1024>,
) -> Result<&'static str, Error> {
Ok("Hello, World!")
}
```
Anything that implements `FromRequest` can work as an extractor where
`FromRequest` is a simple async trait:
```rust
#[async_trait]
pub trait FromRequest: Sized {
async fn from_request(req: &mut Request<Body>) -> Result<Self, Error>;
}
```
You can also return different response types:
```rust
async fn string_response(req: Request<Body>) -> Result<String, Error> {
// ...
}
// gets `content-type: appliation/json`. `Json` can contain any `T: Serialize`
async fn json_response(req: Request<Body>) -> Result<response::Json<User>, Error> {
// ...
}
// gets `content-type: text/html`. `Html` can contain any `T: Into<Bytes>`
async fn html_response(req: Request<Body>) -> Result<response::Html<String>, Error> {
// ...
}
// or for full control
async fn response(req: Request<Body>) -> Result<Response<Body>, Error> {
// ...
}
```
You can also apply Tower middleware to single routes:
```rust
let app = tower_web::app()
.at("/")
.get(send_some_large_file.layer(tower_http::compression::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))
.layer(TraceLayer::new_for_http())
.layer(CompressionLayer::new())
.service(app);
```
And of course run it with Hyper:
```rust
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
// build our application with some routes
let app = tower_web::app()
.at("/")
.get(handler)
// convert it into a `Service`
.into_service();
// add some middleware
let app = ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
.service(app);
// 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();
}
```
See the examples directory for more examples.
# TODO
- Error handling should probably be redone. Not quite sure if its bad the
`Error` is just an enum where everything is public.
- Audit which error codes we return for each kind of error. This will probably
be changed when error handling is re-done.
- Probably don't want to require `hyper::Body` for request bodies. Should
have our own so hyper isn't required.
- `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.

232
examples/lots_of_routes.rs Normal file
View file

@ -0,0 +1,232 @@
use http::Request;
use hyper::Server;
use std::net::SocketAddr;
use tower::make::Shared;
use tower_web::{body::Body, Error};
#[tokio::main]
async fn main() {
// 100 routes should still compile in a reasonable amount of time
// add a .boxed() every 10 routes to improve compile times
let app = tower_web::app()
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.boxed()
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.boxed()
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.boxed()
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.boxed()
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.boxed()
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.boxed()
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.boxed()
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.boxed()
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.boxed()
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.at("/")
.get(handler)
.boxed()
.into_service();
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();
}
async fn handler(_req: Request<Body>) -> Result<&'static str, Error> {
Ok("Hello, World!")
}

View file

@ -26,7 +26,7 @@ impl<D, E> BoxBody<D, E> {
}
}
// TODO(david): upstream this to http-body?
// TODO: upstream this to http-body?
impl<D, E> Default for BoxBody<D, E>
where
D: bytes::Buf + 'static,

View file

@ -49,6 +49,16 @@ pub enum Error {
InvalidUtf8,
}
impl Error {
/// Create an `Error` from a `BoxError` coming from a `Service`
pub(crate) fn from_service_error(error: BoxError) -> Error {
match error.downcast::<Error>() {
Ok(err) => *err,
Err(err) => Error::Service(err),
}
}
}
impl From<Infallible> for Error {
fn from(err: Infallible) -> Self {
match err {}

View file

@ -1,6 +1,6 @@
use self::{
body::Body,
routing::{EmptyRouter, RouteAt},
routing::{AlwaysNotFound, RouteAt},
};
use bytes::Bytes;
use futures_util::ready;
@ -26,15 +26,15 @@ mod tests;
pub use self::error::Error;
pub fn app() -> App<EmptyRouter> {
pub fn app() -> App<AlwaysNotFound> {
App {
router: EmptyRouter(()),
service_tree: AlwaysNotFound(()),
}
}
#[derive(Debug, Clone)]
pub struct App<R> {
router: R,
service_tree: R,
}
impl<R> App<R> {
@ -79,7 +79,7 @@ where
#[inline]
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
if let Err(err) = ready!(self.app.router.poll_ready(cx)).map_err(Into::into) {
if let Err(err) = ready!(self.app.service_tree.poll_ready(cx)).map_err(Into::into) {
self.poll_ready_error = Some(err);
}
@ -97,7 +97,7 @@ where
}
}
}
HandleErrorFuture(Kind::Future(self.app.router.call(req)))
HandleErrorFuture(Kind::Future(self.app.service_tree.call(req)))
}
}

View file

@ -10,16 +10,21 @@ use http::{Method, Request, Response, StatusCode};
use pin_project::pin_project;
use std::{
convert::Infallible,
fmt,
future::Future,
pin::Pin,
task::{Context, Poll},
};
use tower::{BoxError, Service};
use tower::{
buffer::{Buffer, BufferLayer},
util::BoxService,
BoxError, Service, ServiceBuilder,
};
#[derive(Clone, Copy)]
pub struct EmptyRouter(pub(crate) ());
pub struct AlwaysNotFound(pub(crate) ());
impl<R> Service<R> for EmptyRouter {
impl<R> Service<R> for AlwaysNotFound {
type Response = Response<Body>;
type Error = Infallible;
type Future = future::Ready<Result<Self::Response, Self::Error>>;
@ -42,14 +47,14 @@ pub struct RouteAt<R> {
}
impl<R> RouteAt<R> {
pub fn get<F, B, T>(self, handler_fn: F) -> RouteBuilder<Route<HandlerSvc<F, B, T>, R>>
pub fn get<F, B, T>(self, handler_fn: F) -> RouteBuilder<Or<HandlerSvc<F, B, T>, R>>
where
F: Handler<B, T>,
{
self.add_route(handler_fn, Method::GET)
}
pub fn get_service<S, B>(self, service: S) -> RouteBuilder<Route<S, R>>
pub fn get_service<S, B>(self, service: S) -> RouteBuilder<Or<S, R>>
where
S: Service<Request<Body>, Response = Response<B>> + Clone,
S::Error: Into<BoxError>,
@ -57,14 +62,14 @@ impl<R> RouteAt<R> {
self.add_route_service(service, Method::GET)
}
pub fn post<F, B, T>(self, handler_fn: F) -> RouteBuilder<Route<HandlerSvc<F, B, T>, R>>
pub fn post<F, B, T>(self, handler_fn: F) -> RouteBuilder<Or<HandlerSvc<F, B, T>, R>>
where
F: Handler<B, T>,
{
self.add_route(handler_fn, Method::POST)
}
pub fn post_service<S, B>(self, service: S) -> RouteBuilder<Route<S, R>>
pub fn post_service<S, B>(self, service: S) -> RouteBuilder<Or<S, R>>
where
S: Service<Request<Body>, Response = Response<B>> + Clone,
S::Error: Into<BoxError>,
@ -76,24 +81,24 @@ impl<R> RouteAt<R> {
self,
handler: H,
method: Method,
) -> RouteBuilder<Route<HandlerSvc<H, B, T>, R>>
) -> RouteBuilder<Or<HandlerSvc<H, B, T>, R>>
where
H: Handler<B, T>,
{
self.add_route_service(HandlerSvc::new(handler), method)
}
fn add_route_service<S>(self, service: S, method: Method) -> RouteBuilder<Route<S, R>> {
fn add_route_service<S>(self, service: S, method: Method) -> RouteBuilder<Or<S, R>> {
assert!(
self.route_spec.starts_with(b"/"),
"route spec must start with a slash (`/`)"
);
let new_app = App {
router: Route {
service_tree: Or {
service,
route_spec: RouteSpec::new(method, self.route_spec.clone()),
fallback: self.app.router,
fallback: self.app.service_tree,
handler_ready: false,
fallback_ready: false,
},
@ -128,14 +133,14 @@ impl<R> RouteBuilder<R> {
self.app.at(route_spec)
}
pub fn get<F, B, T>(self, handler_fn: F) -> RouteBuilder<Route<HandlerSvc<F, B, T>, R>>
pub fn get<F, B, T>(self, handler_fn: F) -> RouteBuilder<Or<HandlerSvc<F, B, T>, R>>
where
F: Handler<B, T>,
{
self.app.at_bytes(self.route_spec).get(handler_fn)
}
pub fn get_service<S, B>(self, service: S) -> RouteBuilder<Route<S, R>>
pub fn get_service<S, B>(self, service: S) -> RouteBuilder<Or<S, R>>
where
S: Service<Request<Body>, Response = Response<B>> + Clone,
S::Error: Into<BoxError>,
@ -143,14 +148,14 @@ impl<R> RouteBuilder<R> {
self.app.at_bytes(self.route_spec).get_service(service)
}
pub fn post<F, B, T>(self, handler_fn: F) -> RouteBuilder<Route<HandlerSvc<F, B, T>, R>>
pub fn post<F, B, T>(self, handler_fn: F) -> RouteBuilder<Or<HandlerSvc<F, B, T>, R>>
where
F: Handler<B, T>,
{
self.app.at_bytes(self.route_spec).post(handler_fn)
}
pub fn post_service<S, B>(self, service: S) -> RouteBuilder<Route<S, R>>
pub fn post_service<S, B>(self, service: S) -> RouteBuilder<Or<S, R>>
where
S: Service<Request<Body>, Response = Response<B>> + Clone,
S::Error: Into<BoxError>,
@ -164,9 +169,30 @@ impl<R> RouteBuilder<R> {
poll_ready_error: None,
}
}
pub fn boxed<B>(self) -> RouteBuilder<BoxServiceTree<B>>
where
R: Service<Request<Body>, Response = Response<B>, Error = Error> + Send + 'static,
R::Future: Send,
B: Default + 'static,
{
let svc = ServiceBuilder::new()
.layer(BufferLayer::new(1024))
.layer(BoxService::layer())
.service(self.app.service_tree);
let app = App {
service_tree: BoxServiceTree { inner: svc },
};
RouteBuilder {
app,
route_spec: self.route_spec,
}
}
}
pub struct Route<H, F> {
pub struct Or<H, F> {
service: H,
route_spec: RouteSpec,
fallback: F,
@ -174,7 +200,7 @@ pub struct Route<H, F> {
fallback_ready: bool,
}
impl<H, F> Clone for Route<H, F>
impl<H, F> Clone for Or<H, F>
where
H: Clone,
F: Clone,
@ -242,7 +268,7 @@ impl RouteSpec {
}
}
impl<H, F, HB, FB> Service<Request<Body>> for Route<H, F>
impl<H, F, HB, FB> Service<Request<Body>> for Or<H, F>
where
H: Service<Request<Body>, Response = Response<HB>>,
H::Error: Into<Error>,
@ -327,6 +353,66 @@ where
}
}
pub struct BoxServiceTree<B> {
inner: Buffer<BoxService<Request<Body>, Response<B>, Error>, Request<Body>>,
}
impl<B> Clone for BoxServiceTree<B> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
}
}
}
impl<B> fmt::Debug for BoxServiceTree<B> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("BoxServiceTree").finish()
}
}
impl<B> Service<Request<Body>> for BoxServiceTree<B>
where
B: 'static,
{
type Response = Response<B>;
type Error = Error;
type Future = BoxServiceTreeResponseFuture<B>;
#[inline]
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx).map_err(Error::from_service_error)
}
#[inline]
fn call(&mut self, req: Request<Body>) -> Self::Future {
BoxServiceTreeResponseFuture {
inner: self.inner.call(req),
}
}
}
#[pin_project]
pub struct BoxServiceTreeResponseFuture<B> {
#[pin]
inner: InnerFuture<B>,
}
type InnerFuture<B> = tower::buffer::future::ResponseFuture<
Pin<Box<dyn Future<Output = Result<Response<B>, Error>> + Send + 'static>>,
>;
impl<B> Future for BoxServiceTreeResponseFuture<B> {
type Output = Result<Response<B>, Error>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
self.project()
.inner
.poll(cx)
.map_err(Error::from_service_error)
}
}
#[cfg(test)]
mod tests {
#[allow(unused_imports)]

View file

@ -250,7 +250,35 @@ async fn extracting_url_params() {
assert_eq!(res.status(), StatusCode::OK);
}
// TODO(david): lots of routes and boxing, shouldn't take forever to compile
#[tokio::test]
async fn boxing() {
let app = app()
.at("/")
.get(|_: Request<Body>| async { Ok("hi from GET") })
.boxed()
.post(|_: Request<Body>| async { Ok("hi from POST") })
.into_service();
let addr = run_in_background(app).await;
let client = reqwest::Client::new();
let res = client
.get(format!("http://{}", addr))
.send()
.await
.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.text().await.unwrap(), "hi from GET");
let res = client
.post(format!("http://{}", addr))
.send()
.await
.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.text().await.unwrap(), "hi from POST");
}
/// Run a `tower::Service` in the background and get a URI for it.
pub async fn run_in_background<S, ResBody>(svc: S) -> SocketAddr