mirror of
https://github.com/tokio-rs/axum.git
synced 2026-08-27 00:00:24 +02:00
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:
+90
-93
@@ -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;
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user