Skip to content

Consider changing middleware errors to Box<dyn Error> #131

@seanmonstar

Description

@seanmonstar

The Problem

Most of the various middlewares in tower define an enum Error<E> { Inner(E), .. } to return from Service. A problem starts to arise once you compose multiple middlewares together, but still want to be somewhat generic over handling the errors. Look at this gem:

error: Inner(Inner(Inner(B(Service(Inner(A(B(Error { kind: Connect, details: "interesting" }))))))))

In this case, we've wrapped a hyper::Client in a bunch of middleware. In order to inspect and react to the error (in my case, decide if the error qualifies to retry the request), I could:

  • Set my expect error type exactly, and thus use match e { UnNestAllTheLayers => () }. This is very fragile, since adding new middleware, or changing the order, will cause compilation to fail. It's also hugely tedious being sure we've match every correct combination.
  • Make some trait CanRetry and implement it for every single intermediary type. Adding a new middleware means we need to implement CanRetry for the potentially new nested error enum.

Proposal

A different solution might be better: remove the enum Error<E>, and instead having all middleware just return Box<dyn std::error::Error + Send + Sync>.

The code needed to inspect errors if they were dynamic trait objects changes to something like this:

if let Some(e) = error.downcast_ref::<hyper::Error>() {
    if e.is_connect() { // or whatever
        return CanRetry::Yes;
    }
}

Example

Consider tower-in-flight-limit:

impl<S, Req> Service<Req> for InFlightLimit<S>
where
    S: Service<Req>,
    S::Error: Into<Box<dyn StdError + Send + Sync>>,
{
    type Response = S::Response;
    type Error = Box<dyn StdError + Send + Sync>;

    // ...
}

impl<T> Future for ResponseFuture<T>
where
    T: Future,
    T::Error: Into<Box<dyn StdError + Send + Sync>>,
{
    type Item = T::Item;
    type Error = Box<dyn StdError + Send + Sync>;

    fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
        match self.inner {
            Some(ref mut f) => {
                match f.poll() {
                    // ...
                    Err(e) => {
                        self.shared.release();
                        Err(e.into())
                    }
                }
            }
            None => Err(NoCapacity.into()),
        }
    }
}

This change means that if an error occurs, it will be a Box<dyn StdError + Send + Sync>, but it won't be nested (unless nesting with Error::cause() (now in 1.30, Error::source()!)), and so it should only be either S::Error or NoCapacity.

By requiring the bounds of S::Error: Into<Box<dyn StdError + Send + Sync>>, this allows only boxing the error once. If multiple middleware are wrapped up, and where they would previously return Error::Inner(e), they'll just return the Box<dyn StdError> without wrapping.

A couple downsides exist with this change:

  • By moving to trait objects, we can no longer rely on Send and Sync being propagated through the generics, so we would need to require S;:Error: Send + Sync. In practice, it seems reasonable that errors would be Send + Sync anyways.
  • This means the error case now causes an allocation, whereas with the enum, it didn't. The error cause should be infrequent, so the allocations shouldn't happen too often. However, this actually can mean an performance improvement, since Result<T, Box<Whatever>> can sometimes remove the need for the tag in Result, if T is zero-sized. Additionally, nested enums grow in memory, so having a single Box can means the Result is smaller regardless.
  • If you aren't nesting middleware, then the sum type enum Error can be nice to ensure you handle every error case, and this proposal would force you to do downcast even in those simple cases.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions