Move methods from ServiceExt to RoutingDsl (#160)

Previously, on `main`, this wouldn't compile:

```rust
let app = route("/", get(handler))
    .layer(
        ServiceBuilder::new()
            .timeout(Duration::from_secs(10))
            .into_inner(),
    )
    .handle_error(...)
    .route(...); // <-- doesn't work
```

That is because `handle_error` would be
`axum::service::ServiceExt::handle_error` which returns `HandleError<_,
_, _, HandleErrorFromService>` which does _not_ implement `RoutingDsl`.
So you couldn't call `route`. This was caused by
https://github.com/tokio-rs/axum/pull/120.

Basically `handle_error` when called on a `RoutingDsl`, the resulting
service should also implement `RoutingDsl`, but if called on another
random service it should _not_ implement `RoutingDsl`.

I don't think thats possible by having `handle_error` on `ServiceExt`
which is implemented for any service, since all axum routers are also
services by design.

This resolves the issue by removing `ServiceExt` and moving its methods
to `RoutingDsl`. Then we have more tight control over what has a
`handle_error` method.

`service::OnMethod` now also has a `handle_error` so you can still
handle errors from random services, by doing
`service::any(svc).handle_error(...)`.
This commit is contained in:
David Pedersen
2021-08-08 14:30:51 +02:00
committed by GitHub
parent 9b3f3c9bdf
commit 8013165908
12 changed files with 346 additions and 297 deletions
+90 -93
View File
@@ -6,7 +6,7 @@ use crate::{
buffer::MpscBuffer,
extract::connect_info::{Connected, IntoMakeServiceWithConnectInfo},
response::IntoResponse,
service::HandleErrorFromRouter,
service::{HandleError, HandleErrorFromRouter},
util::ByteStr,
};
use async_trait::async_trait;
@@ -348,6 +348,94 @@ pub trait RoutingDsl: crate::sealed::Sealed + Sized {
second: other,
}
}
/// Handle errors services in this router might produce, by mapping them to
/// responses.
///
/// Unhandled errors will close the connection without sending a response.
///
/// # Example
///
/// ```
/// use axum::{http::StatusCode, prelude::*};
/// use tower::{BoxError, timeout::TimeoutLayer};
/// use std::{time::Duration, convert::Infallible};
///
/// // This router can never fail, since handlers can never fail.
/// let app = route("/", get(|| async {}));
///
/// // Now the router can fail since the `tower::timeout::Timeout`
/// // middleware will return an error if the timeout elapses.
/// let app = app.layer(TimeoutLayer::new(Duration::from_secs(10)));
///
/// // With `handle_error` we can handle errors `Timeout` might produce.
/// // Our router now cannot fail, that is its error type is `Infallible`.
/// let app = app.handle_error(|error: BoxError| {
/// if error.is::<tower::timeout::error::Elapsed>() {
/// Ok::<_, Infallible>((
/// StatusCode::REQUEST_TIMEOUT,
/// "request took too long to handle".to_string(),
/// ))
/// } else {
/// Ok::<_, Infallible>((
/// StatusCode::INTERNAL_SERVER_ERROR,
/// format!("Unhandled error: {}", error),
/// ))
/// }
/// });
/// # async {
/// # hyper::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap();
/// # };
/// ```
///
/// You can return `Err(_)` from the closure if you don't wish to handle
/// some errors:
///
/// ```
/// use axum::{http::StatusCode, prelude::*};
/// use tower::{BoxError, timeout::TimeoutLayer};
/// use std::time::Duration;
///
/// let app = route("/", get(|| async {}))
/// .layer(TimeoutLayer::new(Duration::from_secs(10)))
/// .handle_error(|error: BoxError| {
/// if error.is::<tower::timeout::error::Elapsed>() {
/// Ok((
/// StatusCode::REQUEST_TIMEOUT,
/// "request took too long to handle".to_string(),
/// ))
/// } else {
/// // return the error as is
/// Err(error)
/// }
/// });
/// # async {
/// # hyper::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap();
/// # };
/// ```
fn handle_error<ReqBody, ResBody, F, Res, E>(
self,
f: F,
) -> HandleError<Self, F, ReqBody, HandleErrorFromRouter>
where
Self: Service<Request<ReqBody>, Response = Response<ResBody>>,
F: FnOnce(Self::Error) -> Result<Res, E>,
Res: IntoResponse,
ResBody: http_body::Body<Data = Bytes> + Send + Sync + 'static,
ResBody::Error: Into<BoxError> + Send + Sync + 'static,
{
HandleError::new(self, f)
}
/// Check that your service cannot fail.
///
/// That is, its error type is [`Infallible`].
fn check_infallible<ReqBody>(self) -> Self
where
Self: Service<Request<ReqBody>, Error = Infallible>,
{
self
}
}
impl<S, F> RoutingDsl for Route<S, F> {}
@@ -646,97 +734,6 @@ impl<S> RoutingDsl for Layered<S> {}
impl<S> crate::sealed::Sealed for Layered<S> {}
impl<S> Layered<S> {
/// Create a new [`Layered`] service where errors will be handled using the
/// given closure.
///
/// This is used to convert errors to responses rather than simply
/// terminating the connection.
///
/// That can be done using `handle_error` like so:
///
/// ```rust
/// use axum::prelude::*;
/// use http::StatusCode;
/// use tower::{BoxError, timeout::TimeoutLayer};
/// use std::{convert::Infallible, time::Duration};
///
/// async fn handler() { /* ... */ }
///
/// // `Timeout` will fail with `BoxError` if the timeout elapses...
/// let layered_app = route("/", get(handler))
/// .layer(TimeoutLayer::new(Duration::from_secs(30)));
///
/// // ...so we should handle that error
/// let with_errors_handled = layered_app.handle_error(|error: BoxError| {
/// if error.is::<tower::timeout::error::Elapsed>() {
/// Ok::<_, Infallible>((
/// StatusCode::REQUEST_TIMEOUT,
/// "request took too long".to_string(),
/// ))
/// } else {
/// Ok::<_, Infallible>((
/// StatusCode::INTERNAL_SERVER_ERROR,
/// format!("Unhandled internal error: {}", error),
/// ))
/// }
/// });
/// # async {
/// # axum::Server::bind(&"".parse().unwrap())
/// # .serve(with_errors_handled.into_make_service())
/// # .await
/// # .unwrap();
/// # };
/// ```
///
/// The closure must return `Result<T, E>` where `T` implements [`IntoResponse`].
///
/// You can also return `Err(_)` if you don't wish to handle the error:
///
/// ```rust
/// use axum::prelude::*;
/// use http::StatusCode;
/// use tower::{BoxError, timeout::TimeoutLayer};
/// use std::time::Duration;
///
/// async fn handler() { /* ... */ }
///
/// let layered_app = route("/", get(handler))
/// .layer(TimeoutLayer::new(Duration::from_secs(30)));
///
/// let with_errors_handled = layered_app.handle_error(|error: BoxError| {
/// if error.is::<tower::timeout::error::Elapsed>() {
/// Ok((
/// StatusCode::REQUEST_TIMEOUT,
/// "request took too long".to_string(),
/// ))
/// } else {
/// // keep the error as is
/// Err(error)
/// }
/// });
/// # async {
/// # axum::Server::bind(&"".parse().unwrap())
/// # .serve(with_errors_handled.into_make_service())
/// # .await
/// # .unwrap();
/// # };
/// ```
pub fn handle_error<F, ReqBody, ResBody, Res, E>(
self,
f: F,
) -> crate::service::HandleError<S, F, ReqBody, HandleErrorFromRouter>
where
S: Service<Request<ReqBody>, Response = Response<ResBody>> + Clone,
F: FnOnce(S::Error) -> Result<Res, E>,
Res: IntoResponse,
ResBody: http_body::Body<Data = Bytes> + Send + Sync + 'static,
ResBody::Error: Into<BoxError> + Send + Sync + 'static,
{
crate::service::HandleError::new(self.inner, f)
}
}
impl<S, R> Service<R> for Layered<S>
where
S: Service<R>,
@@ -809,7 +806,7 @@ where
///
/// ```
/// use axum::{
/// routing::nest, service::{get, ServiceExt}, prelude::*,
/// routing::nest, service::get, prelude::*,
/// };
/// use tower_http::services::ServeDir;
///