From a04fc42d75f4869b2aacc3cecfff6af20e3a8679 Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Sun, 30 Jan 2022 20:09:18 +0100 Subject: [PATCH] Rework middleware docs (#732) * rework middleware docs * Apply suggestions from code review Co-authored-by: Jonas Platte Co-authored-by: Jonas Platte --- axum/src/docs/middleware.md | 402 ++++++++++++++++++++++------- axum/src/docs/routing/layer.md | 73 +----- axum/src/lib.rs | 66 +---- axum/src/middleware/mod.rs | 2 + examples/sqlx-postgres/src/main.rs | 2 +- 5 files changed, 320 insertions(+), 225 deletions(-) diff --git a/axum/src/docs/middleware.md b/axum/src/docs/middleware.md index b51bd833..b7826e43 100644 --- a/axum/src/docs/middleware.md +++ b/axum/src/docs/middleware.md @@ -1,19 +1,24 @@ -axum is designed to take full advantage of the [`tower`] and [`tower-http`] -ecosystem of middleware. +axum is unique in that it doesn't have its own bespoke middleware system and +instead integrates with [`tower`]. This means the ecosystem of [`tower`] and +[`tower-http`] middleware all work with axum. -If you're new to tower we recommend you read its [guides][tower-guides] for -a general introduction to tower and its concepts. +While its not necessary to fully understand tower to write or use middleware +with axum, having at least a basic understanding of tower's concepts is +recommended. See [tower's guides][tower-guides] for a general introduction. +Reading the documentation for [`tower::ServiceBuilder`] is also recommended. -axum supports adding middleware to both individual handlers and entire routers. -For more details on that see +# Applying middleware -- [Individual handlers](crate::handler::Handler::layer) -- [Routers](crate::routing::Router::layer) +axum allows you to add middleware just about anywhere + +- To entire routers with [`Router::layer`] and [`Router::route_layer`]. +- To method routers with [`MethodRouter::layer`] and [`MethodRouter::route_layer`]. +- To individual handlers with [`Handler::layer`]. ## Applying multiple middleware -It's recommended to use [`tower::ServiceBuilder`] to apply multiple middleware at -once, instead of calling [`Router::layer`] repeatedly: +Its recommended to use [`tower::ServiceBuilder`] to apply multiple middleware at +once, instead of calling `layer` (or `route_layer`) repeatedly: ```rust use axum::{ @@ -22,7 +27,7 @@ use axum::{ Router, }; use tower_http::{trace::TraceLayer}; -use tower::{ServiceBuilder, limit::ConcurrencyLimitLayer}; +use tower::ServiceBuilder; async fn handler() {} @@ -34,7 +39,6 @@ let app = Router::new() .layer( ServiceBuilder::new() .layer(TraceLayer::new_for_http()) - .layer(ConcurrencyLimitLayer::new(64)) .layer(AddExtensionLayer::new(State {})) ); # async { @@ -42,95 +46,185 @@ let app = Router::new() # }; ``` -## Middleware and errors +# Commonly used middleware -If you're applying middleware that produces errors you have to handle the errors -so they're converted into responses. You can learn more about doing that -[here](crate::error_handling). +Some commonly used middleware are: -## Commonly used middleware +- [`TraceLayer`](tower_http::trace) for high level tracing/logging. +- [`CorsLayer`](tower_http::cors) for handling CORS. +- [`CompressionLayer`](tower_http::compression) for automatic compression of + responses. +- [`RequestIdLayer`](tower_http::request_id) and + [`PropagateRequestIdLayer`](tower_http::request_id) set and propagate request + ids. +- [`TimeoutLayer`](tower::timeout::TimeoutLayer) for timeouts. Note this + requires using [`HandleErrorLayer`](crate::error_handling::HandleErrorLayer) + to convert timeouts to responses. -[`tower`] and [`tower_http`] have a large collection of middleware that are -compatible with axum. Some commonly used middleware are: +# Ordering -```rust,no_run -use axum::{ - response::Response, - Router, - body::{Body, BoxBody}, - error_handling::HandleErrorLayer, - http::Request, - routing::get, -}; -use tower::{ - filter::AsyncFilterLayer, - util::AndThenLayer, - ServiceBuilder, -}; -use std::convert::Infallible; -use tower_http::trace::TraceLayer; +When you add middleware with [`Router::layer`] (or similar) all previously added +routes will be wrapped in the middleware. Generally speaking, this results in +middleware being executed from bottom to top. + +So if you do this: + +```rust +use axum::{routing::get, Router}; + +async fn handler() {} + +# let layer_one = axum::AddExtensionLayer::new(()); +# let layer_two = axum::AddExtensionLayer::new(()); +# let layer_three = axum::AddExtensionLayer::new(()); # -# async fn handle_error(error: T) -> axum::http::StatusCode { -# axum::http::StatusCode::INTERNAL_SERVER_ERROR -# } - -let middleware_stack = ServiceBuilder::new() - // Handle errors from middleware - // - // This middleware most be added above any fallible - // ones if you're using `ServiceBuilder`, due to how ordering works - .layer(HandleErrorLayer::new(handle_error)) - // `TraceLayer` adds high level tracing and logging - .layer(TraceLayer::new_for_http()) - // `AsyncFilterLayer` lets you asynchronously transform the request - .layer(AsyncFilterLayer::new(map_request)) - // `AndThenLayer` lets you asynchronously transform the response - .layer(AndThenLayer::new(map_response)); - -async fn map_request(req: Request) -> Result, Infallible> { - Ok(req) -} - -async fn map_response(res: Response) -> Result { - Ok(res) -} - let app = Router::new() - .route("/", get(|| async { /* ... */ })) - .layer(middleware_stack); -# async { -# axum::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap(); -# }; + .route("/", get(handler)) + .layer(layer_one) + .layer(layer_two) + .layer(layer_three); +# let app: Router = app; ``` -Additionally axum provides [`extract::extractor_middleware()`] for converting -any extractor into a middleware. See [`extract::extractor_middleware()`] for -more details. +Think of the middleware as being layered like an onion where each new layer +wraps all previous layers: -## Writing your own middleware with `axum_extra::middleware::from_fn` +```not_rust + requests + | + v ++----- layer_three -----+ +| +---- layer_two ----+ | +| | +-- layer_one --+ | | +| | | | | | +| | | handler | | | +| | | | | | +| | +-- layer_one --+ | | +| +---- layer_two ----+ | ++----- layer_three -----+ + | + v + responses +``` -The easiest way to write a custom middleware is using -[`axum_extra::middleware::from_fn`]. See that function for more details. +That is: -[`axum_extra::middleware::from_fn`]: https://docs.rs/axum-extra/0.1/axum_extra/middleware/middleware_fn/fn.from_fn.html +- First `layer_three` receives the request +- It then does its thing and passes the request onto `layer_two` +- Which passes the request onto `layer_one` +- Which passes the request onto `handler` where a response is produced +- That response is then passes to `layer_one` +- Then to `layer_two` +- And finally to `layer_three` where it's returned out of your app -## Writing your own middleware with `tower::Service` +It's a little more complicated in practice because any middleware is free to +return early and not call the next layer, for example if a request cannot be +authorized, but its a useful mental model to have. + +As previously mentioned its recommended to add multiple middleware using +`tower::ServiceBuilder`, however this impacts ordering: + +```rust +use tower::ServiceBuilder; +use axum::{routing::get, Router}; + +async fn handler() {} + +# let layer_one = axum::AddExtensionLayer::new(()); +# let layer_two = axum::AddExtensionLayer::new(()); +# let layer_three = axum::AddExtensionLayer::new(()); +# +let app = Router::new() + .route("/", get(handler)) + .layer( + ServiceBuilder::new() + .layer(layer_one) + .layer(layer_two) + .layer(layer_three), + ); +# let app: Router = app; +``` + +`ServiceBuilder` works by composing all layers into one such that they run top +to bottom. So with the previous code `layer_one` would receive the request +first, then `layer_two`, then `layer_three`, then `handler`, and then the +response would bubble back up through `layer_three`, then `layer_two`, and +finally `layer_one`. + +Executing middleware top to bottom is generally easier to understand and follow +mentally which is one of the reasons `ServiceBuilder` is recommended. + +# Writing middleware + +axum offers many ways of writing middleware, at different levels of abstraction +and with different pros and cons. + +## `axum::middleware::from_fn` + +Use [`axum::middleware::from_fn`] to write your middleware when: + +- You're not comfortable with implementing your own futures and would rather use + the familiar `async`/`await` syntax. +- You don't intend to publish your middleware as a crate for others to use. + Middleware written like this are only compatible with axum. + +## `axum::extract::extractor_middleware` + +Use [`axum::extract::extractor_middleware`] to write your middleware when: + +- You have a type that you sometimes want to use as an extractor and sometimes + as a middleware. If you only need your type as a middleware prefer + [`middleware::from_fn`]. + +## tower's combinators + +tower has several utility combinators that can be used to perform simple +modifications to requests or responses. The most commonly used ones are + +- [`ServiceBuilder::map_request`] +- [`ServiceBuilder::map_response`] +- [`ServiceBuilder::then`] +- [`ServiceBuilder::and_then`] + +You should use these when + +- You want to perform a small ad hoc operation, such as adding a header. +- You don't intend to publish your middleware as a crate for others to use. + +# `tower::Service` and `Pin>` For maximum control (and a more low level API) you can write you own middleware by implementing [`tower::Service`]: +Use [`tower::Service`] with `Pin>` to write your middleware when: + +- Your middleware needs to be configurable for example via builder methods on + your [`tower::Layer`] such as [`tower_http::trace::TraceLayer`]. +- You do intend to publish your middleware as a crate for others to use. +- You're not comfortable with implementing your own futures. + +A decent template for such a middleware could be: + ```rust use axum::{ response::Response, - Router, - body::{Body, BoxBody}, + body::Body, http::Request, - routing::get, }; use futures::future::BoxFuture; -use tower::{Service, layer::layer_fn}; +use tower::{Service, Layer}; use std::task::{Context, Poll}; +struct MyLayer; + +impl Layer for MyLayer { + type Service = MyMiddleware; + + fn layer(&self, inner: S) -> Self::Service { + MyMiddleware { inner } + } +} + #[derive(Clone)] struct MyMiddleware { inner: S, @@ -138,39 +232,161 @@ struct MyMiddleware { impl Service> for MyMiddleware where - S: Service, Response = Response> + Clone + Send + 'static, + S: Service, Response = Response> + Send + 'static, S::Future: Send + 'static, { type Response = S::Response; type Error = S::Error; + // `BoxFuture` is a type alias for `Pin>` type Future = BoxFuture<'static, Result>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.inner.poll_ready(cx) } - fn call(&mut self, mut req: Request) -> Self::Future { - println!("`MyMiddleware` called!"); - - // best practice is to clone the inner service like this - // see https://github.com/tower-rs/tower/issues/547 for details - let clone = self.inner.clone(); - let mut inner = std::mem::replace(&mut self.inner, clone); - + fn call(&mut self, mut request: Request) -> Self::Future { + let future = self.inner.call(request); Box::pin(async move { - let res: Response = inner.call(req).await?; - - println!("`MyMiddleware` received the response"); - - Ok(res) + let response: Response = future.await?; + Ok(response) }) } } +``` + +# `tower::Service` and custom futures + +If you're comfortable implementing your own futures (or want to learn it) and +need as much control as possible then using `tower::Service` without boxed +futures is the way to go. + +Use [`tower::Service`] with manual futures to write your middleware when: + +- You want your middleware to have the lowest possible overhead. +- Your middleware needs to be configurable for example via builder methods on + your [`tower::Layer`] such as [`tower_http::trace::TraceLayer`]. +- You do intend to publish your middleware as a crate for others to use, perhaps + as part of tower-http. +- You're comfortable with implementing your own futures, or want to learn how + the lower levels of async Rust works. + +tower's ["Building a middleware from scratch"][tower-from-scratch-guide] +guide is a good place to learn how to do this. + +# Error handling for middleware + +axum's error handling model requires handlers to always return a response. +However middleware is one possible way to introduce errors into an application. +If hyper receives an error the connection will be closed without sending a +response. Thus axum requires those errors to be handled gracefully: + +```rust +use axum::{ + routing::get, + error_handling::HandleErrorLayer, + http::StatusCode, + BoxError, + Router, +}; +use tower::{ServiceBuilder, timeout::TimeoutLayer}; +use std::time::Duration; + +async fn handler() {} let app = Router::new() - .route("/", get(|| async { /* ... */ })) - .layer(layer_fn(|inner| MyMiddleware { inner })); + .route("/", get(handler)) + .layer( + ServiceBuilder::new() + // this middleware goes above `TimeoutLayer` because it will receive + // errors returned by `TimeoutLayer` + .layer(HandleErrorLayer::new(|_: BoxError| async { + StatusCode::REQUEST_TIMEOUT + })) + .layer(TimeoutLayer::new(Duration::from_secs(10))) + ); # async { # axum::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap(); # }; ``` + +See [`error_handling`](crate::error_handling) for more details on axum's error +handling model. + +# Routing to services/middleware and backpressure + +Generally routing to one of multiple services and backpressure doesn't mix +well. Ideally you would want ensure a service is ready to receive a request +before calling it. However, in order to know which service to call, you need +the request... + +One approach is to not consider the router service itself ready until all +destination services are ready. That is the approach used by +[`tower::steer::Steer`]. + +Another approach is to always consider all services ready (always return +`Poll::Ready(Ok(()))`) from `Service::poll_ready` and then actually drive +readiness inside the response future returned by `Service::call`. This works +well when your services don't care about backpressure and are always ready +anyway. + +axum expects that all services used in your app wont care about +backpressure and so it uses the latter strategy. However that means you +should avoid routing to a service (or using a middleware) that _does_ care +about backpressure. At the very least you should [load shed] so requests are +dropped quickly and don't keep piling up. + +It also means that if `poll_ready` returns an error then that error will be +returned in the response future from `call` and _not_ from `poll_ready`. In +that case, the underlying service will _not_ be discarded and will continue +to be used for future requests. Services that expect to be discarded if +`poll_ready` fails should _not_ be used with axum. + +One possible approach is to only apply backpressure sensitive middleware +around your entire app. This is possible because axum applications are +themselves services: + +```rust +use axum::{ + routing::get, + Router, +}; +use tower::ServiceBuilder; +# let some_backpressure_sensitive_middleware = +# tower::layer::util::Identity::new(); + +async fn handler() { /* ... */ } + +let app = Router::new().route("/", get(handler)); + +let app = ServiceBuilder::new() + .layer(some_backpressure_sensitive_middleware) + .service(app); +# async { +# axum::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap(); +# }; +``` + +However when applying middleware around your whole application in this way +you have to take care that errors are still being handled with +appropriately. + +Also note that handlers created from async functions don't care about +backpressure and are always ready. So if you're not using any Tower +middleware you don't have to worry about any of this. + +[`tower`]: https://crates.io/crates/tower +[`tower-http`]: https://crates.io/crates/tower-http +[tower-guides]: https://github.com/tower-rs/tower/tree/master/guides +[`axum::middleware::from_fn`]: crate::middleware::from_fn +[`middleware::from_fn`]: crate::middleware::from_fn +[tower-from-scratch-guide]: https://github.com/tower-rs/tower/blob/master/guides/building-a-middleware-from-scratch.md +[`ServiceBuilder::map_request`]: tower::ServiceBuilder::map_request +[`ServiceBuilder::map_response`]: tower::ServiceBuilder::map_response +[`ServiceBuilder::then`]: tower::ServiceBuilder::then +[`ServiceBuilder::and_then`]: tower::ServiceBuilder::and_then +[`axum::extract::extractor_middleware`]: crate::extract::extractor_middleware() +[`Handler::layer`]: crate::handler::Handler::layer +[`Router::layer`]: crate::routing::Router::layer +[`MethodRouter::layer`]: crate::routing::MethodRouter::layer +[`Router::route_layer`]: crate::routing::Router::route_layer +[`MethodRouter::route_layer`]: crate::routing::MethodRouter::route_layer diff --git a/axum/src/docs/routing/layer.md b/axum/src/docs/routing/layer.md index af316b21..f659babb 100644 --- a/axum/src/docs/routing/layer.md +++ b/axum/src/docs/routing/layer.md @@ -6,7 +6,7 @@ corresponding middleware. This can be used to add additional processing to a request for a group of routes. -Note this differs from [`handler::Layered`](crate::handler::Layered) +Note this differs from [`Handler::layer`](crate::handler::Handler::layer) which adds a middleware to a single handler. # Example @@ -66,73 +66,12 @@ let app = Router::new() # }; ``` -# Applying multiple middleware +# Multiple middleware -Its recommended to use [`tower::ServiceBuilder`] to apply multiple middleware at -once, instead of calling `layer` repeatedly: - -```rust -use axum::{ - routing::get, - AddExtensionLayer, - Router, -}; -use tower_http::{trace::TraceLayer}; -use tower::{ServiceBuilder, limit::ConcurrencyLimitLayer}; - -async fn handler() {} - -#[derive(Clone)] -struct State {} - -let app = Router::new() - .route("/", get(handler)) - .layer( - ServiceBuilder::new() - .layer(TraceLayer::new_for_http()) - .layer(ConcurrencyLimitLayer::new(64)) - .layer(AddExtensionLayer::new(State {})) - ); -# async { -# axum::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap(); -# }; -``` +It's recommended to use [`tower::ServiceBuilder`] when applying multiple +middleware. See [`middleware`](crate::middleware) for more details. # Error handling -axum's error handling model requires handlers to always return a response. -However middleware is one possible way to introduce errors into an application. -If hyper receives an error the connection will be closed without sending a -response. Thus axum requires those errors to be handled gracefully: - -```rust -use axum::{ - routing::get, - error_handling::HandleErrorLayer, - http::StatusCode, - BoxError, - Router, -}; -use tower::{ServiceBuilder, timeout::TimeoutLayer}; -use std::time::Duration; - -async fn handler() {} - -let app = Router::new() - .route("/", get(handler)) - .layer( - ServiceBuilder::new() - // this middleware goes above `TimeoutLayer` because it will receive - // errors returned by `TimeoutLayer` - .layer(HandleErrorLayer::new(|_: BoxError| async { - StatusCode::REQUEST_TIMEOUT - })) - .layer(TimeoutLayer::new(Duration::from_secs(10))) - ); -# async { -# axum::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap(); -# }; -``` - -See [`error_handling`](crate::error_handling) for more details on axum's error -handling model. +See [`middleware`](crate::middleware) for details on how error handling impacts +middleware. diff --git a/axum/src/lib.rs b/axum/src/lib.rs index 64d0144a..f421c573 100644 --- a/axum/src/lib.rs +++ b/axum/src/lib.rs @@ -11,7 +11,6 @@ //! - [Responses](#responses) //! - [Error handling](#error-handling) //! - [Middleware](#middleware) -//! - [Routing to services and backpressure](#routing-to-services-and-backpressure) //! - [Sharing state with handlers](#sharing-state-with-handlers) //! - [Building integrations for axum](#building-integrations-for-axum) //! - [Required dependencies](#required-dependencies) @@ -160,69 +159,8 @@ //! //! # Middleware //! -#![doc = include_str!("docs/middleware.md")] -//! -//! # Routing to services and backpressure -//! -//! Generally routing to one of multiple services and backpressure doesn't mix -//! well. Ideally you would want ensure a service is ready to receive a request -//! before calling it. However, in order to know which service to call, you need -//! the request... -//! -//! One approach is to not consider the router service itself ready until all -//! destination services are ready. That is the approach used by -//! [`tower::steer::Steer`]. -//! -//! Another approach is to always consider all services ready (always return -//! `Poll::Ready(Ok(()))`) from `Service::poll_ready` and then actually drive -//! readiness inside the response future returned by `Service::call`. This works -//! well when your services don't care about backpressure and are always ready -//! anyway. -//! -//! axum expects that all services used in your app wont care about -//! backpressure and so it uses the latter strategy. However that means you -//! should avoid routing to a service (or using a middleware) that _does_ care -//! about backpressure. At the very least you should [load shed] so requests are -//! dropped quickly and don't keep piling up. -//! -//! It also means that if `poll_ready` returns an error then that error will be -//! returned in the response future from `call` and _not_ from `poll_ready`. In -//! that case, the underlying service will _not_ be discarded and will continue -//! to be used for future requests. Services that expect to be discarded if -//! `poll_ready` fails should _not_ be used with axum. -//! -//! One possible approach is to only apply backpressure sensitive middleware -//! around your entire app. This is possible because axum applications are -//! themselves services: -//! -//! ```rust -//! use axum::{ -//! routing::get, -//! Router, -//! }; -//! use tower::ServiceBuilder; -//! # let some_backpressure_sensitive_middleware = -//! # tower::layer::util::Identity::new(); -//! -//! async fn handler() { /* ... */ } -//! -//! let app = Router::new().route("/", get(handler)); -//! -//! let app = ServiceBuilder::new() -//! .layer(some_backpressure_sensitive_middleware) -//! .service(app); -//! # async { -//! # axum::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap(); -//! # }; -//! ``` -//! -//! However when applying middleware around your whole application in this way -//! you have to take care that errors are still being handled with -//! appropriately. -//! -//! Also note that handlers created from async functions don't care about -//! backpressure and are always ready. So if you're not using any Tower -//! middleware you don't have to worry about any of this. +//! There are several different ways to write middleware for axum. See +//! [`middleware`](crate::middleware) for more details. //! //! # Sharing state with handlers //! diff --git a/axum/src/middleware/mod.rs b/axum/src/middleware/mod.rs index cd0cd349..2a31a48b 100644 --- a/axum/src/middleware/mod.rs +++ b/axum/src/middleware/mod.rs @@ -1,4 +1,6 @@ //! Utilities for writing middleware +//! +#![doc = include_str!("../docs/middleware.md")] mod from_fn; diff --git a/examples/sqlx-postgres/src/main.rs b/examples/sqlx-postgres/src/main.rs index 655f9c74..31c94f81 100644 --- a/examples/sqlx-postgres/src/main.rs +++ b/examples/sqlx-postgres/src/main.rs @@ -1,4 +1,4 @@ -//! Example of application using https://github.com/launchbadge/sqlx +//! Example of application using //! //! Run with //!