All Articles

Secure Error Handling With Actix-Web

Introduction

Imagine someone made an api call, logging into their account with a misspelled email address:

curl --data { "email": "jJohn@example.org", "password" : "asdf1234" }

Imagine if they got this response:

{ "error": "passwords do not match for User {
  email: jJohn@example.org,
  phone: 404 873 9099,
  address: 1234 baker street,
  password: sS#*)!MSJ(09adAHD's}io#
} "}

Although you might not think about it often, it is very possible for your application to leak sensitive data through error messages, In this post, I will walk you through how I handle error messages in my Rust (actix-web) web applications.

Custom Error Type

In my Rust applications, I generally like to have a single error type for each domain that encompasses all the possible errors that can occur:

#[derive(Debug)]
pub enum MyError {
  Response(ResponseError),
  Io(std::io::Error),
  Db(db::error::Error),
}

I implement for the standard Error trait for my error type:

impl std::error::Error for Error {}

I also fmt::Display by simple calling fmt() on the respective enum variant:

impl fmt::Display for MyError {
  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    match *self {
      MyError::Response(ref e) => e.fmt(f),
      MyError::Io(ref e) => e.fmt(f),
      MyError::Db(ref e) => e.fmt(f),
    }
  }
}

I then implement the Into conversion for each variant. This will allow the use of the ? operator in functions that return Result:

impl From<db::error::Error> for MyError {
  fn from(err: db::error::Error) -> MyError {
    MyError::Db(err)
  }
}

impl From<std::io::Error> for Error {
  fn from(err: std::io::Error) -> MyError {
    MyError::Io(err)
  }
}

While there is a decent amount of boilerplate involved in this, I don't see the need to introduce a third-party crate for simple error conversions.

Response Error

All of MyError's variants are only used for internal logging. For actual error responses, only the Response variant is used. Response holds a custom ResponseError type:

#[derive(Debug)]
pub struct ResponseError {
  pub code: u16,
  pub error: ResponseErrorKind,
}

ResponseError is a struct which contains a status code and a ResponseErrorKind, which is also an enum. In this simple example, it contains one variant, which is a optional string message:

#[derive(Debug, Clone)]
pub enum ResponseErrorKind {
  Message(Option<&'static str>),
}

The std::fmt::Display implementation returns a json error response:

impl fmt::Display for ResponseError {
  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    match &self.error {
      ResponseErrorKind::Message(m) => match m {
        Some(m) => format!("{{ \"error\": {} }}", m).fmt(f),
        None => Ok(()),
      }
    }
  }
}

If the response error contains a message, it will display a json response that looks like this:

{ "error": "The Error Message" }

Actix-Web Response Error

For actix_web to be able to display the error response properly, the error type must implement the actix_web::ResponseError trait. ResponseError has two required methods, status_code, and error_response.

The status_code method for the custom error type will either return the Response variant's status code, or 500 (internal server error):

use actix_web::http::StatusCode;

impl actix_web::ResponseError for MyError {
  fn status_code(&self) -> StatusCode {
    match *self {
      Error::Response(err) => StatusCode::from_u16(err.code).unwrap(),
      _ => StatusCode::INTERNAL_SERVER_ERROR,
    }
  }
}

The error_response method will either return the json error message of the Response variant, or { "error": "An unexpected error occured" }:

use actix_web::{dev::HttpResponseBuilder, http::StatusCode, HttpResponse};

impl actix_web::ResponseError for MyError {
  fn error_response(&self) -> HttpResponse {
    match *self {
      Error::Response(err) => HttpResponseBuilder::new(StatusCode::from_u16(err.code).unwrap())
        .content_type("application/json")
        .body(err.to_string()),
      _ => HttpResponseBuilder::new(StatusCode::INTERNAL_SERVER_ERROR)
        .content_type("application/json")
        .body("{{ \"error\": \"An unexpected error occured\" }}"),
    }
  }
}

Now, I create some convenience methods for ResponseError:

impl ResponseError {
  pub fn message(code: u16, msg: &'static str) -> Self {
    ResponseError {
      code,
      error: ResponseErrorKind::Message(Some(msg)),
    }
  }

  pub fn code(code: u16) -> Self {
    ResponseError {
      code,
      error: ResponseErrorKind::Message(None),
    }
  }

Usage

That's it for the MyError. Now, I can use it in my handlers. For example, a handler that returns a ResponseError:

async fn hello() -> Result<HttpResponse, Error> {
  ResponseError::message(404, "Invalid Request")?
}

Will result in the following json response:

[404] { "error": "Invalid Request" }

However, when making a sensitive database call, you can still use the ? operator for cleaner error handling:

async fn login() -> Result<HttpResponse, Error> {
  make_sensitive_db_query()?
}

And no sensitive information will be leaked:

[500] { "error": "An unexpected error occured" }

If you want to add some additional context to an error, you can simply map it to a ResponseError:

async fn login() -> Result<HttpResponse, Error> {
  make_sensitive_db_query()
    .map_err(|_| ResponseError::message(401, "Invalid Login Attempt"))?
}
[401] { "error": "Invalid Login Attempt" }