warp: Error Handling Examples

It’s possible I’m just not following the rejections example well enough, but what is the correct way of indicating that a handler (called with and_then()) is fallible, and returning the error from that method in a reply?

For an example of the behaviour I’m trying to replicate, in Actix or Rouille, handler functions are defined as returning something resembling a Result<GoodReply, BadReply>, both of which can be expected to produce an HTTP response.

e.g a simple index handler which should generate a 500 if the templating fails:

async fn index(tmpl: web::Data<tera::Tera>) -> Result<HttpResponse, Error> {
    let html = tmpl
        .render("index.html", &tera::Context::new())
        .map_err(|_| HttpResponse::InternalServerError("Failed to render template")))?;

    Ok(HttpResponse::Ok().body(html))
}

Reading the docs for Warp, it seems like a Rejection should be used to indicate that this filter could not or should not accept the request, is this the case? And if so, is there an idiomatic way to replicate the behaviour I’ve sketched above? I can go down the road of matching on results from fallible functions in my handlers, and doing an early return warp::reply(...), but then I lose out on the ergonomics offered by the ? operator inside the function.

About this issue

  • Original URL
  • State: open
  • Created 4 years ago
  • Reactions: 24
  • Comments: 28 (3 by maintainers)

Commits related to this issue

Most upvoted comments

Rejections are meant to say a Filter couldn’t fulfill its preconditions, but maybe another Filter can. If a filter is otherwise fully matched, and an error occurs in your business logic, it’s probably not correct to reject with the error. In that case, you’d want to construct a Reply that describes your error.

In the todos example, once the request has gotten through the filters and arrived to the handler function, it chooses to return responses with the correct status codes instead of rejecting.

It may be worth adding some pattern to ease that?

Rejections are meant to say a Filter couldn’t fulfill its preconditions, but maybe another Filter can. If a filter is otherwise fully matched, and an error occurs in your business logic, it’s probably not correct to reject with the error. In that case, you’d want to construct a Reply that describes your error.

I think this is what I needed to read, I’ve apparently been going about this incorrectly the whole time.

It may be worth adding some pattern to ease that?

I would definitely be interested in that, though I’m not sure what it would look like. I’ve only been using warp for a few weeks (tracking master pre-0.2), and I was initially relatively confused about what the proper way to deal with routes failing was. Rejections seemed silly since I wouldn’t want to try the entire rest of the tree just to have it fail to match.

Despite that, I’ve mistakenly (apparently; thanks for the clarification, the repo being small enough to watch fully has been helpful) found myself rejecting with warp::reject::custom(ApiError::Variant) so far, and looking for ApiError in a recover filter. Maybe the annoying verbosity of warp::reject::custom should have ticked me off 😄.

Without rejections + recover however, it does somewhat seem like there’s a gap when it comes to unified error handling? Of course, every individual route could call a function to build an error response, but having this everywhere:

let foo = match get_foo().await {
    Ok(v) => v,
    Err(e) => return Ok(some_error_response(e)),
}

… would be very unpleasant, and even worse than .map_err(|e| warp::reject::custom(SomeVariant(e)))?. Additionally, unified logging of errors becomes a lot harder!

In some ways it feels natural, in others silly, to have the route handler filters (by choice) be <Extract = (Result<Reply, MyError>,), Rejection = !>, and then using an .ok_or_else() adapter on the top-level filter to translate MyError to another Reply. Not sure if this would be a good idea.

I agree with what has been mentioned before, it would be nice

  • if there would be a way to not always go through all filters (like having a conclusive filter that is the immediate response no matter where in the chain it occurs), and
  • to be able to use ? (which currently implies rejecting).

Speaking from the experience of using warp for quite a while now (and I love it, great engineering work!). For almost all API routes, once I get past the path filter, I don’t want other filters to get executed at all (though I still think the default of trying the other filters is good). This does not only apply to the actual business logic of the route, but also to filters that are part of it (that define its parameters). Considering the following example (the comments describe what I’d like to achieve):

let a = warp::post() // if this fails, continue with `b`
    .and(path!("upload")) // if this fails, continue with `b`
    .and(authenticated()) // if this fails, don't continue with any other filter
    .and(warp::body::content_length_limit(1024 * 1024 * 10)) // if this fails, don't continue with any other filter
    .and(warp::multipart::form()) // if this fails, don't continue with any other filter
    .and_then(routes::upload); // if this fails, don't continue with any other filter
let b = warp::get()// ...
a.or(b)

If path!("upload") was successful, but one of the three succeeding filters (authentication, content length limit and multipart form) rejects, I want it to stop right there and don’t try executing any other filter/route. I of course also want to stop executing other filters if routes::upload itself rejected (using rejects there due to the ergonomics of ?).

Fortunately, I’ve found a way to achieve this with built-in filters utilizing the current state of rejects:

let a = warp::post().and(
    // Everything starting here will be covered by the `recover` below
    path!("upload")
        // return a custom error if the path did not match so we can uniquely identify this case
        .or_else(|_| async { Err(reject::custom(PathMismatch)) })
        .and(authenticated())
        .and(warp::body::content_length_limit(1024 * 1024 * 10))
        .and(warp::multipart::form())
        .and_then(routes::upload)
        // The recover will handle all rejects that happened starting with `path!`
        .recover(recover),
);

Here is a reduced version of my recover function:

async fn recover(err: warp::Rejection) -> Result<impl warp::Reply, warp::Rejection> {
    if err.find::<PathMismatch>().is_some() {
        // This is the only case where we actually want to reject and continue with the other
        // filters in the filter chain. Otherwise, the execution would stop at the first `path`
        // that did not match the request.
        Err(reject::not_found())
    } else if let Some(ref err) = err.find::<error::ClientError>() {
        // An example of a custom error type I am using to return structured errors
        Ok(
            warp::reply::with_status(warp::reply::json(err), StatusCode::BAD_REQUEST)
                .into_response(),
        )
    } else if let Some(ref err) = err.find::<error::ServerError>() {
        // Another custom error type, this time for server errors
        log::error!("{}", err);
        Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response())
    } else if err.find::<warp::reject::PayloadTooLarge>().is_some() {
        // An example of converting a rejection of a built-in filter
        // (`warp::body::content_length_limit` in this case) into a structured error
        Ok(warp::reply::with_status(
            warp::reply::json(&error::client_error(
                "file_size",
                "File size limit of 10MB exceeded",
            )),
            StatusCode::PAYLOAD_TOO_LARGE,
        )
        .into_response())
    } else {
        // My business logic always returns either one of my custom error types `ClientError`
        // and `ServerError`, so I safely assume that all other rejects are due to filters that
        // extract data from the request, which is why I simply return a 404 in those cases.
        // It is important to return an `Ok` here, because again, no other filters should be
        // executed afterwards.
        Ok(StatusCode::NOT_FOUND.into_response())
    }
}

#[derive(Debug)]
struct PathMismatch;

impl reject::Reject for PathMismatch {}

Maybe this helps others to achieve their intended behaviour.

Hi, if I understand correctly you question, to return early with a custom Rejection you have to create an Error, which has to impl warp::reject::Reject, and wrap it under warp::reject::custom. taking your example:

enum MyError {
    TeraError(tera::Error)
}
impl warp::reject::Reject for MyError {}

async fn index(tmpl: web::Data<tera::Tera>) -> Result<HttpResponse, Rejection> {
    let html = tmpl
        .render("index.html", &tera::Context::new())
        .map_err(|err| warp::reject::custom(MyError::TeraError(err)))?;

    Ok(HttpResponse::Ok().body(html))
}

you can then convert this Rejection into a Reply using the recover filter, or else this will be returned as a 500 Internal Server Error. the rejections example shows how to recover from a Rejection and convert it to a Reply according to each custom Rejection

In the todos example, once the request has gotten through the filters and arrived to the handler function, it chooses to return responses with the correct status codes instead of rejecting.

It may be worth adding some pattern to ease that?

I landed here looking for a pattern where I could use ?; to return an error from my handlers, but not go the rejection route, as there is no sense moving onto another handler, so some pattern to handle this would be very nice.

I’ve solved this by creating this Result wrapper, but I agree it would be nice to have this functionality in warp. Not being able to return Result, the error variant of which triggers an Internal Server Error, is quite unintuitive.

use http::StatusCode;
use std::error::Error;
use warp::reply::Reply;
use warp::reply::{html, with_status, Response};

const INTERNAL_SERVER_ERROR: &'static str = "Internal server error";

pub struct ReplyResult<T, E>(Result<T, E>);

impl<T: Reply, E: Send + Error> Reply for ReplyResult<T, E> {
    fn into_response(self) -> Response {
        match self.0 {
            Ok(x) => x.into_response(),
            Err(e) => {
                warn!("Filter failed: {}", e);
                with_status(
                    html(INTERNAL_SERVER_ERROR),
                    StatusCode::INTERNAL_SERVER_ERROR,
                )
                .into_response()
            }
        }
    }
}

impl<T, E> From<Result<T, E>> for ReplyResult<T, E> {
    fn from(value: Result<T, E>) -> ReplyResult<T, E> {
        ReplyResult(value)
    }
}

#[inline]
pub fn catch_error<T, E>(result: Result<T, E>) -> ReplyResult<T, E> { ReplyResult(result) }

It can be used by simply adding a single map with the function path:

use std::io::{Error, ErrorKind};
let route = warp::path!("guess" / isize)
    .map(|n| match n {
        0 => Ok("Secret unlocked"),
        _ => Err(Error::from(ErrorKind::Other))
    })
    .map(catch_error);

I’m also uncertain how one should handle filters required for a route? If I match a path and decide on its route, but need a database connection to handle the request, I don’t want to pointlessly try matching other paths if I fail to acquire one; I want to send a 500 response immediately. A thorough explanation of how what kinds of errors should be and are handled would be very appreciated.

@benitogf

this method is not available on version 0.3?

no method named `into_response` found for opaque type `impl warp::Reply` in the current scope

method not found in `impl warp::Reply`

help: items from traits can only be used if the trait is in scope

It is, you are probably just missing a use warp::Reply (as the error suggests).

Btw #458 implements Reply for Result in warp and I’ve been using it successfully in https://github.com/cjbassi/rust-warp-realworld-backend.

I’ve been able to get the error handling working with a custom error type by converting my error to a Rejection in the handler and then converting it to a Response in the recover filter, but the ergonomics are not great. For instance, there is a lot of .map_err(MyError::from)? instead of just ?.

It seems like it’s a common pattern to not return a Rejection once you get to the handler, and instead return error responses. It would be nice if there was a way to return a Result in the handlers that implements Reply . Unfortunately, I was not able to get this to work because you cannot impl an external trait for a custom Result that is aliased to std::result::Result.

edit: Would it be possible to add an impl Reply for Result types in warp itself? Or maybe add a custom result type in warp for this?