From f6b1a6f4359c06cd656a92eaac5dbf8a6ce11c65 Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Mon, 31 May 2021 16:28:26 +0200 Subject: [PATCH] More work --- Cargo.toml | 2 +- README.md | 217 ++++++++++++++++++++++++++++++++++ examples/lots_of_routes.rs | 232 +++++++++++++++++++++++++++++++++++++ src/body.rs | 2 +- src/error.rs | 10 ++ src/lib.rs | 12 +- src/routing.rs | 122 ++++++++++++++++--- src/tests.rs | 30 ++++- 8 files changed, 600 insertions(+), 27 deletions(-) create mode 100644 README.md create mode 100644 examples/lots_of_routes.rs diff --git a/Cargo.toml b/Cargo.toml index 692abda5..a1376f2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } diff --git a/README.md b/README.md new file mode 100644 index 00000000..cdfdd9ef --- /dev/null +++ b/README.md @@ -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` 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) -> 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) -> 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, + // deserialize response body with `serde_json` into a `UserPayload` + user: extract::Json, + // deserialize query string into a `Pagination` + pagination: extract::Query, +) -> 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, + // buffer the whole request body + body: Bytes, +) -> Result<&'static str, Error> { + // ... +} +``` + +Or limit the body size: + +```rust +async fn handler( + req: Request, + // 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) -> Result; +} +``` + +You can also return different response types: + +```rust +async fn string_response(req: Request) -> Result { + // ... +} + +// gets `content-type: appliation/json`. `Json` can contain any `T: Serialize` +async fn json_response(req: Request) -> Result, Error> { + // ... +} + +// gets `content-type: text/html`. `Html` can contain any `T: Into` +async fn html_response(req: Request) -> Result, Error> { + // ... +} + +// or for full control +async fn response(req: Request) -> Result, 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. diff --git a/examples/lots_of_routes.rs b/examples/lots_of_routes.rs new file mode 100644 index 00000000..96b8ad71 --- /dev/null +++ b/examples/lots_of_routes.rs @@ -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) -> Result<&'static str, Error> { + Ok("Hello, World!") +} diff --git a/src/body.rs b/src/body.rs index 44152377..0075fdfc 100644 --- a/src/body.rs +++ b/src/body.rs @@ -26,7 +26,7 @@ impl BoxBody { } } -// TODO(david): upstream this to http-body? +// TODO: upstream this to http-body? impl Default for BoxBody where D: bytes::Buf + 'static, diff --git a/src/error.rs b/src/error.rs index 9ad1e527..877661c6 100644 --- a/src/error.rs +++ b/src/error.rs @@ -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::() { + Ok(err) => *err, + Err(err) => Error::Service(err), + } + } +} + impl From for Error { fn from(err: Infallible) -> Self { match err {} diff --git a/src/lib.rs b/src/lib.rs index d8c3e46a..8f815236 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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 { +pub fn app() -> App { App { - router: EmptyRouter(()), + service_tree: AlwaysNotFound(()), } } #[derive(Debug, Clone)] pub struct App { - router: R, + service_tree: R, } impl App { @@ -79,7 +79,7 @@ where #[inline] fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - 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))) } } diff --git a/src/routing.rs b/src/routing.rs index 6597b78a..2c3d128c 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -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 Service for EmptyRouter { +impl Service for AlwaysNotFound { type Response = Response; type Error = Infallible; type Future = future::Ready>; @@ -42,14 +47,14 @@ pub struct RouteAt { } impl RouteAt { - pub fn get(self, handler_fn: F) -> RouteBuilder, R>> + pub fn get(self, handler_fn: F) -> RouteBuilder, R>> where F: Handler, { self.add_route(handler_fn, Method::GET) } - pub fn get_service(self, service: S) -> RouteBuilder> + pub fn get_service(self, service: S) -> RouteBuilder> where S: Service, Response = Response> + Clone, S::Error: Into, @@ -57,14 +62,14 @@ impl RouteAt { self.add_route_service(service, Method::GET) } - pub fn post(self, handler_fn: F) -> RouteBuilder, R>> + pub fn post(self, handler_fn: F) -> RouteBuilder, R>> where F: Handler, { self.add_route(handler_fn, Method::POST) } - pub fn post_service(self, service: S) -> RouteBuilder> + pub fn post_service(self, service: S) -> RouteBuilder> where S: Service, Response = Response> + Clone, S::Error: Into, @@ -76,24 +81,24 @@ impl RouteAt { self, handler: H, method: Method, - ) -> RouteBuilder, R>> + ) -> RouteBuilder, R>> where H: Handler, { self.add_route_service(HandlerSvc::new(handler), method) } - fn add_route_service(self, service: S, method: Method) -> RouteBuilder> { + fn add_route_service(self, service: S, method: Method) -> RouteBuilder> { 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 RouteBuilder { self.app.at(route_spec) } - pub fn get(self, handler_fn: F) -> RouteBuilder, R>> + pub fn get(self, handler_fn: F) -> RouteBuilder, R>> where F: Handler, { self.app.at_bytes(self.route_spec).get(handler_fn) } - pub fn get_service(self, service: S) -> RouteBuilder> + pub fn get_service(self, service: S) -> RouteBuilder> where S: Service, Response = Response> + Clone, S::Error: Into, @@ -143,14 +148,14 @@ impl RouteBuilder { self.app.at_bytes(self.route_spec).get_service(service) } - pub fn post(self, handler_fn: F) -> RouteBuilder, R>> + pub fn post(self, handler_fn: F) -> RouteBuilder, R>> where F: Handler, { self.app.at_bytes(self.route_spec).post(handler_fn) } - pub fn post_service(self, service: S) -> RouteBuilder> + pub fn post_service(self, service: S) -> RouteBuilder> where S: Service, Response = Response> + Clone, S::Error: Into, @@ -164,9 +169,30 @@ impl RouteBuilder { poll_ready_error: None, } } + + pub fn boxed(self) -> RouteBuilder> + where + R: Service, Response = Response, 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 { +pub struct Or { service: H, route_spec: RouteSpec, fallback: F, @@ -174,7 +200,7 @@ pub struct Route { fallback_ready: bool, } -impl Clone for Route +impl Clone for Or where H: Clone, F: Clone, @@ -242,7 +268,7 @@ impl RouteSpec { } } -impl Service> for Route +impl Service> for Or where H: Service, Response = Response>, H::Error: Into, @@ -327,6 +353,66 @@ where } } +pub struct BoxServiceTree { + inner: Buffer, Response, Error>, Request>, +} + +impl Clone for BoxServiceTree { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +impl fmt::Debug for BoxServiceTree { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("BoxServiceTree").finish() + } +} + +impl Service> for BoxServiceTree +where + B: 'static, +{ + type Response = Response; + type Error = Error; + type Future = BoxServiceTreeResponseFuture; + + #[inline] + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx).map_err(Error::from_service_error) + } + + #[inline] + fn call(&mut self, req: Request) -> Self::Future { + BoxServiceTreeResponseFuture { + inner: self.inner.call(req), + } + } +} + +#[pin_project] +pub struct BoxServiceTreeResponseFuture { + #[pin] + inner: InnerFuture, +} + +type InnerFuture = tower::buffer::future::ResponseFuture< + Pin, Error>> + Send + 'static>>, +>; + +impl Future for BoxServiceTreeResponseFuture { + type Output = Result, Error>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + self.project() + .inner + .poll(cx) + .map_err(Error::from_service_error) + } +} + #[cfg(test)] mod tests { #[allow(unused_imports)] diff --git a/src/tests.rs b/src/tests.rs index 5a1012d7..c6b371a1 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -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| async { Ok("hi from GET") }) + .boxed() + .post(|_: Request| 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(svc: S) -> SocketAddr