From 698b5ccf39fa9fee16751ce1718e8d2b920382ef Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Fri, 1 Jul 2022 10:24:15 +0200 Subject: [PATCH 1/6] Update trybuilds for rust 1.62 --- .../debug_handler/fail/argument_not_extractor.stderr | 10 ++++++++++ .../tests/debug_handler/fail/wrong_return_type.stderr | 10 ++++++++++ .../tests/typed_path/fail/not_deserialize.stderr | 10 ++++++++++ 3 files changed, 30 insertions(+) diff --git a/axum-macros/tests/debug_handler/fail/argument_not_extractor.stderr b/axum-macros/tests/debug_handler/fail/argument_not_extractor.stderr index 1694a87e..078b2b03 100644 --- a/axum-macros/tests/debug_handler/fail/argument_not_extractor.stderr +++ b/axum-macros/tests/debug_handler/fail/argument_not_extractor.stderr @@ -4,4 +4,14 @@ error[E0277]: the trait bound `bool: FromRequest` is not satisfied 4 | async fn handler(foo: bool) {} | ^^^^ the trait `FromRequest` is not implemented for `bool` | + = help: the following other types implement trait `FromRequest`: + () + (T1, T2) + (T1, T2, T3) + (T1, T2, T3, T4) + (T1, T2, T3, T4, T5) + (T1, T2, T3, T4, T5, T6) + (T1, T2, T3, T4, T5, T6, T7) + (T1, T2, T3, T4, T5, T6, T7, T8) + and 33 others = help: see issue #48214 diff --git a/axum-macros/tests/debug_handler/fail/wrong_return_type.stderr b/axum-macros/tests/debug_handler/fail/wrong_return_type.stderr index 37fba61f..c3ca7e1e 100644 --- a/axum-macros/tests/debug_handler/fail/wrong_return_type.stderr +++ b/axum-macros/tests/debug_handler/fail/wrong_return_type.stderr @@ -4,6 +4,16 @@ error[E0277]: the trait bound `bool: IntoResponse` is not satisfied 4 | async fn handler() -> bool { | ^^^^ the trait `IntoResponse` is not implemented for `bool` | + = help: the following other types implement trait `IntoResponse`: + &'static [u8] + &'static str + () + (Response<()>, R) + (Response<()>, T1, R) + (Response<()>, T1, T2, R) + (Response<()>, T1, T2, T3, R) + (Response<()>, T1, T2, T3, T4, R) + and 123 others note: required by a bound in `__axum_macros_check_handler_into_response::{closure#0}::check` --> tests/debug_handler/fail/wrong_return_type.rs:4:23 | diff --git a/axum-macros/tests/typed_path/fail/not_deserialize.stderr b/axum-macros/tests/typed_path/fail/not_deserialize.stderr index 3c6f6258..7581b399 100644 --- a/axum-macros/tests/typed_path/fail/not_deserialize.stderr +++ b/axum-macros/tests/typed_path/fail/not_deserialize.stderr @@ -4,6 +4,16 @@ error[E0277]: the trait bound `for<'de> MyPath: serde::de::Deserialize<'de>` is 3 | #[derive(TypedPath)] | ^^^^^^^^^ the trait `for<'de> serde::de::Deserialize<'de>` is not implemented for `MyPath` | + = help: the following other types implement trait `serde::de::Deserialize<'de>`: + &'a [u8] + &'a serde_json::raw::RawValue + &'a std::path::Path + &'a str + () + (T0, T1) + (T0, T1, T2) + (T0, T1, T2, T3) + and 138 others = note: required because of the requirements on the impl of `serde::de::DeserializeOwned` for `MyPath` = note: required because of the requirements on the impl of `FromRequest` for `axum::extract::Path` = note: this error originates in the derive macro `TypedPath` (in Nightly builds, run with -Z macro-backtrace for more info) From 34146f31391f3e0185c2c418f1b6e95823c02d8e Mon Sep 17 00:00:00 2001 From: NicolaLS <90551797+NicolaLS@users.noreply.github.com> Date: Fri, 1 Jul 2022 10:37:41 +0200 Subject: [PATCH 2/6] Implement `TryFrom` for `MethodFilter` (#1130) * implement TryFrom for MethodFilter * test for TryFrom for MethodFilter * 'UnsupportedMethod' error type for MethodFilter * Log TryFrom for MethodFilter * adjust docs * move docs Co-authored-by: David Pedersen --- axum/CHANGELOG.md | 2 + axum/src/routing/method_filter.rs | 97 +++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) diff --git a/axum/CHANGELOG.md b/axum/CHANGELOG.md index 82413421..5ec8d097 100644 --- a/axum/CHANGELOG.md +++ b/axum/CHANGELOG.md @@ -16,7 +16,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `/foo/`). That is no longer supported and such requests will now be sent to the fallback. Consider using `axum_extra::routing::RouterExt::route_with_tsr` if you want the old behavior ([#1119]) +- **added** Implement `TryFrom` for `MethodFilter` and use new `NoMatchingMethodFilter` error in case of failure ([#1130]) +[#1130]: https://github.com/tokio-rs/axum/pull/1130 [#1077]: https://github.com/tokio-rs/axum/pull/1077 [#1102]: https://github.com/tokio-rs/axum/pull/1102 [#1119]: https://github.com/tokio-rs/axum/pull/1119 diff --git a/axum/src/routing/method_filter.rs b/axum/src/routing/method_filter.rs index 82e823a5..ca9b0c06 100644 --- a/axum/src/routing/method_filter.rs +++ b/axum/src/routing/method_filter.rs @@ -1,4 +1,9 @@ use bitflags::bitflags; +use http::Method; +use std::{ + fmt, + fmt::{Debug, Formatter}, +}; bitflags! { /// A filter that matches one or more HTTP methods. @@ -21,3 +26,95 @@ bitflags! { const TRACE = 0b100000000; } } + +/// Error type used when converting a [`Method`] to a [`MethodFilter`] fails. +#[derive(Debug)] +pub struct NoMatchingMethodFilter { + method: Method, +} + +impl NoMatchingMethodFilter { + /// Get the [`Method`] that couldn't be converted to a [`MethodFilter`]. + pub fn method(&self) -> &Method { + &self.method + } +} + +impl fmt::Display for NoMatchingMethodFilter { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "no `MethodFilter` for `{}`", self.method.as_str()) + } +} + +impl std::error::Error for NoMatchingMethodFilter {} + +impl TryFrom for MethodFilter { + type Error = NoMatchingMethodFilter; + + fn try_from(m: Method) -> Result { + match m { + Method::DELETE => Ok(MethodFilter::DELETE), + Method::GET => Ok(MethodFilter::GET), + Method::HEAD => Ok(MethodFilter::HEAD), + Method::OPTIONS => Ok(MethodFilter::OPTIONS), + Method::PATCH => Ok(MethodFilter::PATCH), + Method::POST => Ok(MethodFilter::POST), + Method::PUT => Ok(MethodFilter::PUT), + Method::TRACE => Ok(MethodFilter::TRACE), + other => Err(NoMatchingMethodFilter { method: other }), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn from_http_method() { + assert_eq!( + MethodFilter::try_from(Method::DELETE).unwrap(), + MethodFilter::DELETE + ); + + assert_eq!( + MethodFilter::try_from(Method::GET).unwrap(), + MethodFilter::GET + ); + + assert_eq!( + MethodFilter::try_from(Method::HEAD).unwrap(), + MethodFilter::HEAD + ); + + assert_eq!( + MethodFilter::try_from(Method::OPTIONS).unwrap(), + MethodFilter::OPTIONS + ); + + assert_eq!( + MethodFilter::try_from(Method::PATCH).unwrap(), + MethodFilter::PATCH + ); + + assert_eq!( + MethodFilter::try_from(Method::POST).unwrap(), + MethodFilter::POST + ); + + assert_eq!( + MethodFilter::try_from(Method::PUT).unwrap(), + MethodFilter::PUT + ); + + assert_eq!( + MethodFilter::try_from(Method::TRACE).unwrap(), + MethodFilter::TRACE + ); + + assert!(MethodFilter::try_from(http::Method::CONNECT) + .unwrap_err() + .to_string() + .contains("CONNECT")); + } +} From 8c31bee9bccf28720c3b5d89a1d4e554fcb0efff Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 1 Jul 2022 13:35:52 +0200 Subject: [PATCH 3/6] docsrs cfg fixes (#1137) * Remove unused attribute in axum-core * Fix docs.rs package metadata for axum-extra --- axum-core/src/lib.rs | 1 - axum-extra/Cargo.toml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/axum-core/src/lib.rs b/axum-core/src/lib.rs index 6d15aade..63e363e1 100644 --- a/axum-core/src/lib.rs +++ b/axum-core/src/lib.rs @@ -46,7 +46,6 @@ #![deny(unreachable_pub, private_in_public)] #![allow(elided_lifetimes_in_paths, clippy::type_complexity)] #![forbid(unsafe_code)] -#![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(test, allow(clippy::float_cmp))] #[macro_use] diff --git a/axum-extra/Cargo.toml b/axum-extra/Cargo.toml index 47b5ca64..e9abc7fe 100644 --- a/axum-extra/Cargo.toml +++ b/axum-extra/Cargo.toml @@ -58,4 +58,4 @@ tower = { version = "0.4", features = ["util"] } [package.metadata.docs.rs] all-features = true -rustdocflags = ["--cfg", "docsrs"] +rustdoc-args = ["--cfg", "docsrs"] From eff3b716d3edfc4480ccca12268de2e633f47950 Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Sat, 2 Jul 2022 11:44:17 +0200 Subject: [PATCH 4/6] Document running extractors from middleware (#1140) Fixes #1134 --- axum/src/docs/extract.md | 57 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/axum/src/docs/extract.md b/axum/src/docs/extract.md index eedf5ec5..92784a65 100644 --- a/axum/src/docs/extract.md +++ b/axum/src/docs/extract.md @@ -12,6 +12,7 @@ Types and traits for extracting data from requests. - [Defining custom extractors](#defining-custom-extractors) - [Accessing other extractors in `FromRequest` implementations](#accessing-other-extractors-in-fromrequest-implementations) - [Request body extractors](#request-body-extractors) +- [Running extractors from middleware](#running-extractors-from-middleware) # Intro @@ -579,6 +580,62 @@ let app = Router::new() # }; ``` +# Running extractors from middleware + +Extractors can also be run from middleware by making a [`RequestParts`] and +running your extractor: + +```rust +use axum::{ + Router, + middleware::{self, Next}, + extract::{RequestParts, TypedHeader}, + http::{Request, StatusCode}, + response::Response, + headers::authorization::{Authorization, Bearer}, +}; + +async fn auth_middleware( + request: Request, + next: Next, +) -> Result +where + B: Send, +{ + // running extractors requires a `RequestParts` + let mut request_parts = RequestParts::new(request); + + // `TypedHeader>` extracts the auth token but + // `RequestParts::extract` works with anything that implements `FromRequest` + let auth = request_parts.extract::>>() + .await + .map_err(|_| StatusCode::UNAUTHORIZED)?; + + if !token_is_valid(auth.token()) { + return Err(StatusCode::UNAUTHORIZED); + } + + // get the request back so we can run `next` + // + // `try_into_request` will fail if you have extracted the request body. We + // know that `TypedHeader` never does that. + // + // see the `consume-body-in-extractor-or-middleware` example if you need to + // extract the body + let request = request_parts.try_into_request().expect("body extracted"); + + Ok(next.run(request).await) +} + +fn token_is_valid(token: &str) -> bool { + // ... + # false +} + +let app = Router::new().layer(middleware::from_fn(auth_middleware)); +# let _: Router = app; +``` + [`body::Body`]: crate::body::Body [customize-extractor-error]: https://github.com/tokio-rs/axum/blob/main/examples/customize-extractor-error/src/main.rs [`HeaderMap`]: https://docs.rs/http/latest/http/header/struct.HeaderMap.html From f8c8f5b697c02c67bd455b5602da1a8408a1e06f Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Sat, 2 Jul 2022 11:46:38 +0200 Subject: [PATCH 5/6] Add note about breaking changes to readme --- axum/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/axum/README.md b/axum/README.md index 8fcb9cc1..b78148c3 100644 --- a/axum/README.md +++ b/axum/README.md @@ -23,6 +23,12 @@ In particular the last point is what sets `axum` apart from other frameworks. authorization, and more, for free. It also enables you to share middleware with applications written using [`hyper`] or [`tonic`]. +## Breaking changes + +We are currently working towards axum 0.6 so the `main` branch contains breaking +changes. See the [`0.5.x`] branch for whats released to crates.io and up to date +changelogs. + ## Usage example ```rust @@ -159,3 +165,4 @@ additional terms or conditions. [showcases]: https://github.com/tokio-rs/axum/blob/main/ECOSYSTEM.md#project-showcase [tutorials]: https://github.com/tokio-rs/axum/blob/main/ECOSYSTEM.md#tutorials [license]: https://github.com/tokio-rs/axum/blob/main/axum/LICENSE +[`0.5.x`]: https://github.com/tokio-rs/axum/tree/0.5.x From 2e80ebd18d5c642c7a3ec0e35e0c2f8ab5055e7c Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Sat, 11 Jun 2022 21:19:52 +0200 Subject: [PATCH 6/6] Support running extractors from `middleware::from_fn` --- axum/CHANGELOG.md | 4 +- axum/src/middleware/from_fn.rs | 219 +++++++++++++++++++++++---------- 2 files changed, 158 insertions(+), 65 deletions(-) diff --git a/axum/CHANGELOG.md b/axum/CHANGELOG.md index 5ec8d097..1c9c7d8a 100644 --- a/axum/CHANGELOG.md +++ b/axum/CHANGELOG.md @@ -17,11 +17,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 the fallback. Consider using `axum_extra::routing::RouterExt::route_with_tsr` if you want the old behavior ([#1119]) - **added** Implement `TryFrom` for `MethodFilter` and use new `NoMatchingMethodFilter` error in case of failure ([#1130]) +- **added:** Support running extractors from `middleware::from_fn` functions ([#1088]) -[#1130]: https://github.com/tokio-rs/axum/pull/1130 [#1077]: https://github.com/tokio-rs/axum/pull/1077 +[#1088]: https://github.com/tokio-rs/axum/pull/1088 [#1102]: https://github.com/tokio-rs/axum/pull/1102 [#1119]: https://github.com/tokio-rs/axum/pull/1119 +[#1130]: https://github.com/tokio-rs/axum/pull/1130 [#924]: https://github.com/tokio-rs/axum/pull/924 # 0.5.10 (28. June, 2022) diff --git a/axum/src/middleware/from_fn.rs b/axum/src/middleware/from_fn.rs index 9f9563b5..8e46fa69 100644 --- a/axum/src/middleware/from_fn.rs +++ b/axum/src/middleware/from_fn.rs @@ -3,13 +3,15 @@ use crate::{ response::{IntoResponse, Response}, BoxError, }; +use axum_core::extract::{FromRequest, RequestParts}; +use futures_util::future::BoxFuture; use http::Request; -use pin_project_lite::pin_project; use std::{ any::type_name, convert::Infallible, fmt, future::Future, + marker::PhantomData, pin::Pin, task::{Context, Poll}, }; @@ -23,8 +25,8 @@ use tower_service::Service; /// `from_fn` requires the function given to /// /// 1. Be an `async fn`. -/// 2. Take [`Request`](http::Request) as the first argument. -/// 3. Take [`Next`](Next) as the second argument. +/// 2. Take one or more [extractors] as the first arguments. +/// 3. Take [`Next`](Next) as the final argument. /// 4. Return something that implements [`IntoResponse`]. /// /// # Example @@ -62,6 +64,37 @@ use tower_service::Service; /// # let app: Router = app; /// ``` /// +/// # Running extractors +/// +/// ```rust +/// use axum::{ +/// Router, +/// extract::{TypedHeader, Query}, +/// headers::authorization::{Authorization, Bearer}, +/// http::Request, +/// middleware::{self, Next}, +/// response::Response, +/// routing::get, +/// }; +/// use std::collections::HashMap; +/// +/// async fn my_middleware( +/// TypedHeader(auth): TypedHeader>, +/// Query(query_params): Query>, +/// req: Request, +/// next: Next, +/// ) -> Response { +/// // do something with `auth` and `query_params`... +/// +/// next.run(req).await +/// } +/// +/// let app = Router::new() +/// .route("/", get(|| async { /* ... */ })) +/// .route_layer(middleware::from_fn(my_middleware)); +/// # let app: Router = app; +/// ``` +/// /// # Passing state /// /// State can be passed to the function like so: @@ -114,11 +147,10 @@ use tower_service::Service; /// struct State { /* ... */ } /// /// async fn my_middleware( +/// Extension(state): Extension, /// req: Request, /// next: Next, /// ) -> Response { -/// let state: &State = req.extensions().get().unwrap(); -/// /// // ... /// # ().into_response() /// } @@ -134,8 +166,13 @@ use tower_service::Service; /// ); /// # let app: Router = app; /// ``` -pub fn from_fn(f: F) -> FromFnLayer { - FromFnLayer { f } +/// +/// [extractors]: crate::extract::FromRequest +pub fn from_fn(f: F) -> FromFnLayer { + FromFnLayer { + f, + _extractor: PhantomData, + } } /// A [`tower::Layer`] from an async function. @@ -143,26 +180,41 @@ pub fn from_fn(f: F) -> FromFnLayer { /// [`tower::Layer`] is used to apply middleware to [`Router`](crate::Router)'s. /// /// Created with [`from_fn`]. See that function for more details. -#[derive(Clone, Copy)] -pub struct FromFnLayer { +pub struct FromFnLayer { f: F, + _extractor: PhantomData T>, } -impl Layer for FromFnLayer +impl Clone for FromFnLayer where F: Clone, { - type Service = FromFn; + fn clone(&self) -> Self { + Self { + f: self.f.clone(), + _extractor: self._extractor, + } + } +} + +impl Copy for FromFnLayer where F: Copy {} + +impl Layer for FromFnLayer +where + F: Clone, +{ + type Service = FromFn; fn layer(&self, inner: S) -> Self::Service { FromFn { f: self.f.clone(), inner, + _extractor: PhantomData, } } } -impl fmt::Debug for FromFnLayer { +impl fmt::Debug for FromFnLayer { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("FromFnLayer") // Write out the type name, without quoting it as `&type_name::()` would @@ -174,50 +226,94 @@ impl fmt::Debug for FromFnLayer { /// A middleware created from an async function. /// /// Created with [`from_fn`]. See that function for more details. -#[derive(Clone, Copy)] -pub struct FromFn { +pub struct FromFn { f: F, inner: S, + _extractor: PhantomData T>, } -impl Service> for FromFn +impl Clone for FromFn where - F: FnMut(Request, Next) -> Fut, - Fut: Future, - Out: IntoResponse, - S: Service, Response = Response, Error = Infallible> - + Clone - + Send - + 'static, - S::Future: Send + 'static, - ResBody: HttpBody + Send + 'static, - ResBody::Error: Into, + F: Clone, + S: Clone, { - type Response = Response; - type Error = Infallible; - type Future = ResponseFuture; - - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.inner.poll_ready(cx) - } - - fn call(&mut self, req: Request) -> Self::Future { - let not_ready_inner = self.inner.clone(); - let ready_inner = std::mem::replace(&mut self.inner, not_ready_inner); - - let inner = ServiceBuilder::new() - .boxed_clone() - .map_response_body(body::boxed) - .service(ready_inner); - let next = Next { inner }; - - ResponseFuture { - inner: (self.f)(req, next), + fn clone(&self) -> Self { + Self { + f: self.f.clone(), + inner: self.inner.clone(), + _extractor: self._extractor, } } } -impl fmt::Debug for FromFn +impl Copy for FromFn +where + F: Copy, + S: Copy, +{ +} + +macro_rules! impl_service { + ( $($ty:ident),* $(,)? ) => { + #[allow(non_snake_case)] + impl Service> for FromFn + where + F: FnMut($($ty),*, Next) -> Fut + Clone + Send + 'static, + $( $ty: FromRequest + Send, )* + Fut: Future + Send + 'static, + Out: IntoResponse + 'static, + S: Service, Response = Response, Error = Infallible> + + Clone + + Send + + 'static, + S::Future: Send + 'static, + ReqBody: Send + 'static, + ResBody: HttpBody + Send + 'static, + ResBody::Error: Into, + { + type Response = Response; + type Error = Infallible; + type Future = ResponseFuture; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, req: Request) -> Self::Future { + let not_ready_inner = self.inner.clone(); + let ready_inner = std::mem::replace(&mut self.inner, not_ready_inner); + + let mut f = self.f.clone(); + + let future = Box::pin(async move { + let mut parts = RequestParts::new(req); + $( + let $ty = match $ty::from_request(&mut parts).await { + Ok(value) => value, + Err(rejection) => return rejection.into_response(), + }; + )* + + let inner = ServiceBuilder::new() + .boxed_clone() + .map_response_body(body::boxed) + .service(ready_inner); + let next = Next { inner }; + + f($($ty),*, next).await.into_response() + }); + + ResponseFuture { + inner: future + } + } + } + }; +} + +all_the_tuples!(impl_service); + +impl fmt::Debug for FromFn where S: fmt::Debug, { @@ -252,27 +348,22 @@ impl fmt::Debug for Next { } } -pin_project! { - /// Response future for [`FromFn`]. - pub struct ResponseFuture { - #[pin] - inner: F, +/// Response future for [`FromFn`]. +pub struct ResponseFuture { + inner: BoxFuture<'static, Response>, +} + +impl Future for ResponseFuture { + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + self.inner.as_mut().poll(cx).map(Ok) } } -impl Future for ResponseFuture -where - F: Future, - Out: IntoResponse, -{ - type Output = Result; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - self.project() - .inner - .poll(cx) - .map(IntoResponse::into_response) - .map(Ok) +impl fmt::Debug for ResponseFuture { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ResponseFuture").finish() } }