Crate explicit_error_http

Source
Expand description

Built on top of explicit-error, it provides idiomatic tools to manage errors that generate an HTTP response. Based on the explicit-error crate, its chore tenet is to favor explicitness by inlining the error output while remaining concise.

The key features are:

  • Explicitly mark any error wrapped in a Result as a Fault. A backtrace is captured and a 500 Internal Server HTTP response generated.
  • A derive macro HttpError to easily declare how enum or struct errors transform into an Error, i.e. defines the generated HTTP response.
  • Inline transformation of any errors wrapped in a Result into an Error.
  • Add context to errors to help debug.
  • Monitor errors before they are transformed into proper HTTP responses. The implementation is different depending on the web framework used, to have more details refer to the Web frameworks section.

§A tour of explicit-error-http

The cornerstone of the library is the Error type. Use Result<T, explicit_error_http::Error>, or equivalently explicit_error_http::Result<T>, as the return type of any faillible function returning errors that convert to an HTTP response. Usually, it is mostly functions either called by handlers or middlewares.

§Inline

In the body of the function you can explicitly turn errors into HTTP response using HttpError or marking them as Fault.

use actix_web::http::StatusCode;
use problem_details::ProblemDetails;
use http::Uri;
use explicit_error_http::{prelude::*, HttpError, Result, Fault};
// Import the prelude to enable functions on std::result::Result

fn business_logic() -> Result<()> {
    Err(std::io::Error::new(std::io::ErrorKind::Other, "oh no!"))
        .or_fault()?;

    // Same behavior as fault() but the error is not captured as a source because it does not implement `[std::error::Error]`
    Err("error message").or_fault_no_source()?;

    if 1 > 2 {
        Err(Fault::new()
            .with_context("Usefull context to help debug."))?;
    }

    Err(42).map_err(|_|
        HttpError::new(
            StatusCode::BAD_REQUEST,
            ProblemDetails::new()
                .with_type(Uri::from_static("/errors/business-logic"))
                .with_title("Informative feedback for the user."),
        )
    )?;

    Ok(())
}

Note: The crate problem_details is used as an example for the HTTP response body. You can, of course, use whatever you would like that implements Serialize.

§Enum and struct

Domain errors are often represented as enum or struct as they are raised in different places. To easily enable the conversion to Error use the HttpError derive and implement From<&MyError> for HttpError.

use actix_web::http::StatusCode;
use problem_details::ProblemDetails;
use http::Uri;
use explicit_error_http::{prelude::*, Result, derive::HttpError, HttpError};

#[derive(HttpError, Debug)]
enum MyError {
    Foo,
}

impl From<&MyError> for HttpError {
    fn from(value: &MyError) -> Self {
        match value {
            MyError::Foo => HttpError::new(
                    StatusCode::BAD_REQUEST,
                    ProblemDetails::new()
                        .with_type(Uri::from_static("/errors/my-domain/foo"))
                        .with_title("Foo format incorrect.")
                ),
        }
    }
}

fn business_logic() -> Result<()> {
    Err(MyError::Foo)?;

    Ok(())
}

Note: The HttpError derive implements the conversion to Error, the impl of Display (json format) and std::error::Error.

§Pattern matching

One of the drawbacks of using one and only one return type for different domain functions is that callers loose the ability to pattern match on the returned error. A solution is provided using try_map_on_source on any Result<T, Error>, or equivalently explicit_error_http::Result<T>.

#[derive(HttpError, Debug)]
enum MyError {
    Foo,
    Bar,
}


fn handler() -> Result<()> {
    let err: Result<()> = Err(MyError::Foo)?;
    
    // Do the map if the source's type of the Error is MyError
    err.try_map_on_source(|e| {
        match e {
            MyError::Foo => HttpError::new(
                StatusCode::FORBIDDEN,
                ProblemDetails::new()
                    .with_type(Uri::from_static("/errors/forbidden"))
               ),
            MyError::Bar => HttpError::new(
                StatusCode::UNAUTHORIZED,
                ProblemDetails::new()
                    .with_type(Uri::from_static("/errors/unauthorized"))
               ),
        }
    })?;

    Ok(())
}

Note: under the hood try_map_on_source perform some downcasting.

§Web frameworks

explicit-error-http integrates well with most popular web frameworks by providing a feature flag for each of them.

§Actix web

The type Error cannot directly be used as handlers or middlewares returned Err variant. A dedicated type is required. The easiest implementation is to declare a Newtype, derive it with the HandlerError and implement the HandlerError trait.

#[derive(HandlerErrorHelpers)]
struct MyHandlerError(Error);

impl HandlerError for MyHandlerError {
    // Used by the derive for conversion
    fn from_error(value: Error) -> Self {
        MyHandlerError(value)
    }

    // Set-up monitoring and your custom HTTP response body for faults
    fn public_fault_response(fault: &Fault) -> impl Serialize {
        #[cfg(debug_assertions)]
        error!("{fault}");

        #[cfg(not(debug_assertions))]
        error!("{}", serde_json::json!(faul));

        ProblemDetails::new()
            .with_type(http::Uri::from_static("/errors/internal-server-error"))
            .with_title("Internal server error")
    }

    fn error(&self) -> &Error {
        &self.0
    }

    // Monitor domain variant of your errors and eventually override their body
    fn domain_response(error: &explicit_error_http::DomainError) -> impl Serialize {
        if error.output.http_status_code.as_u16() < 500 {
            debug!("{error}");
        } else {
            error!("{error}");
        }
        error
    }
}

#[get("/my-handler")]
async fn my_handler() -> Result<HttpResponse, MyHandlerError> {
    Ok(HttpResponse::Ok().finish())
}

Modules§

derive
prelude

Structs§

DomainError
Wrapper for errors that are not a Fault. It is used as the explicit_error::Error::Domain variant generic type.
Fault
Re-import from explicit_error crate. Wrapper for errors that should not happen but cannot panic. It is wrapped in the Error::Fault variant.
HttpError
Self-sufficient container to both log an error and generate its HTTP response. Regarding the web framework you use, its shape can be different.

Traits§

HandlerError
The type Error cannot directly be used as handlers or middlewares returned Err variant. A dedicated type is required. The easiest implementation is to declare a Newtype, derive it with the HandlerErrorHelpers and implement the HandlerError trait.
ResultDomainWithContext
To use this trait on Result import the prelude use explicit_error_http::prelude::*
ToDomainError
Internally used by HttpError derive.

Type Aliases§

Error
Result