edrevo
edrevo

Reputation: 1433

What is the most idiomatic way of handling errors in asynchronous handlers in actix-web?

I have an async handler in actix_web that must fail if several headers are not set. I don't understand what the best way of handling errors in functions that return Future should be. I basically want an equivalent of the ? operator for futures.

This is my current code:

r.post().with_async(
    move |req: HttpRequest, path: Path<EventPath>, body: Json<EventCreationRequest>| {
        let headers = req.headers();
        let client_id = match headers
            .get("x-client-id")
            .ok_or("Header not found")
            .and_then(|v| v.to_str().map_err(|_| "Invalid header content"))
        {
            Err(e) => return ok(HttpResponse::BadRequest().body(e)).responder(),
            Ok(v) => v.to_string(),
        };
        operation_that_returns_future()
            .map(|_| HttpResponse::Ok().body("OK!"))
            .responder()
    },
);

I have solved the lack of a ? operator for futures by matching an doing an early return. However, in my code I actually need to ensure that a bunch of other headers exist.

Ideally, I would like to extract the matching and early return logic to something reusable, but in this case that forces me to create a macro. That seems like a bit of an overkill, especially if there's already something in the language that allows me to do what I want.

What is the most idiomatic way of handling this situation?

Upvotes: 2

Views: 819

Answers (1)

arve0
arve0

Reputation: 3647

To handle errors, return a failing Future. For example, do the header check as a Future, then chain your futures with .and_then. A trick is to keep error types of futures the same, to avoid map_err. For example:

fn handler(req: HttpRequest) -> impl Future<Item = HttpResponse, Error = Error> {
    has_client_header(&req)
        .and_then(|client| operation_that_returns_future(client))
        .map(|result| HttpResponse::Ok().body(result))
}

fn has_client_header(req: &HttpRequest) -> impl Future<Item = String, Error = Error> {
    if let Some(Ok(client)) = req.headers().get("x-client-id").map(|h| h.to_str()) {
        future::ok(client.to_owned())
    } else {
        future::failed(ErrorBadRequest("invalid x-client-id header"))
    }
}

fn operation_that_returns_future(client: String) -> impl Future<Item = String, Error = Error> {
    future::ok(client)
}

Result:

$ curl localhost:8000
invalid x-client-id header⏎
$ curl localhost:8000 -H 'x-client-id: asdf'
asdf⏎

When operation_that_returns_future has another error type:

fn handler(req: HttpRequest) -> impl Future<Item = HttpResponse, Error = Error> {
    has_client_header(&req)
        .and_then(|client| {
            operation_that_returns_future(client)
                .map_err(|_| ErrorInternalServerError("operation failed"))
        })
        .map(|result| HttpResponse::Ok().body(result))
}

Another trick is to use the failure crate, which provides failure::Error::from that maps all errors to one type, failure::Error.

Lastly, you might find actix_web::guards useful for checking header values:

.guard(guard::Header("x-client-id", "special client"))

Upvotes: 3

Related Questions