DungeonTiger
DungeonTiger

Reputation: 717

How to properly handle errors from Warp route

I'm trying to structure a non-trivial warp REST application into modules while handling errors and rejections elegantly. It's not working out the way I expect. Consider this simple application:

main.rs

use warp_sample::routes;

#[tokio::main]
async fn main() {
    warp::serve(routes::convert_route()).run(([127, 0, 0, 1], 8080)).await;
}

routes.rs

use warp::{Filter, Rejection, Reply};
use crate::handlers;

pub fn convert_route() -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
    warp::path("convert")
        .and(warp::path::param::<String>())
        .map(|v: String| {
            handlers::convert_str(&v).or_else(|e| e)
        })
}

handlers.rs

use warp::{Reply, Rejection};

pub fn convert_str(s: &str) -> Result<impl Reply, Rejection>{
    match s.parse::<i32>() {
        Ok(s) => Ok(warp::reply()),
        Err(_) => Err(warp::reject())
    }
}

This does not compile because in routes.rs I am trying to return a Rejection struct in the closure which does not match return type of impl Reply.

How do I get this to work? I have tried to change the return type of convert_route but that does not work with warp::serve.

For bonus marks, can you show me how to include a plain text respond in the handler when successful and a different status code when there is an error?

This does not work for the bonus, but it shows you what I am thinking.

pub fn convert_str(s: &str) -> Result<impl Reply, Rejection>{
    match s.parse::<i32>() {
        Ok(s) => Ok(warp::body(s)),
        Err(_) => Err(warp::Rejection {
            reason: Reason::BadRequest,
        })
    }
}

Upvotes: 3

Views: 2555

Answers (1)

apilat
apilat

Reputation: 1510

You should only use map when the operation you are doing is infallible. If you want to be able to return a rejection, you should use and_then instead. In your example, convert_route compiles correctly when changed to the code below.

pub fn convert_route() -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
    warp::path("convert")
        .and(warp::path::param::<String>())
        .and_then(|v: String| async move { convert_str(&v) })
}

Of course in this example, convert_str is somewhat trivial and convert_route can be rewritten without it by simply changing the type in .and(warp::path::param::<String>()), but I assume the function is more involved in your real code.

For the bonus, you will want to create your own rejection type, which will allow you to handle it using recover. Full code:

use warp::{hyper::StatusCode, reject::Reject, Filter, Rejection, Reply};

pub fn convert_route() -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
    warp::path("convert")
        .and(warp::path::param::<String>())
        .and_then(|v: String| async move { convert_str(&v) })
        .recover(|err: Rejection| async move {
            if let Some(ConversionError) = err.find() {
                Ok(StatusCode::BAD_REQUEST)
            } else {
                Err(err)
            }
        })
}

#[derive(Debug)]
struct ConversionError;
impl Reject for ConversionError {}

pub fn convert_str(s: &str) -> Result<impl Reply, Rejection> {
    match s.parse::<i32>() {
        Ok(s) => Ok(s.to_string()),
        Err(_) => Err(warp::reject::custom(ConversionError)),
    }
}

#[tokio::main]
async fn main() {
    warp::serve(convert_route())
        .run(([127, 0, 0, 1], 8080))
        .await;
}

Upvotes: 2

Related Questions