jeudi 27 février 2020

Map service error in controller without repetition

I'm currently developing a 3-Tier actix web REST API. It works very well, but I'm struggling conceptually on how to handle errors without copy pasting all my match arms and duplicating common errors between my services.

Right now, my application looks like that:

RepositoryA
-> always returns QueryResult (Result<T, Error> from diesel)

ServiceX
-> do_a() returns data or my ErrorEnumA (AlreadyExists,GenericDBError,HashError)
-> do_b() returns data or my ErrorEnumB (GenericDBError)
-> do_c() returns data or my ErrorEnumC (CourseFull,CourseNotExists,GenericDBError)

ControllerG
-> get_t() calls serviceX do_a and do_b and matches the errors to Result<HttpResponse, MyApiError>

MyApiError is a ResponseError, setting the http status and a json body with message and code.

With that architecture, I've got the following issues

  1. Every service method has it's own error enum. GenericDBError is in all of them.
  2. I often have a 1:1 mapping in the controller from service error to MyApiError (e.g. HashError would map to {500, "", 5012 (my application error code)}

Since everything has it's own enum, I can also not share a mapping from ErrorEnum to ApiError. Merging all service error into a big enum also is not possible, as the caller would never know which of them might be given back by the service he's currently calling.

That means my controller with only one method already looks like this:

#[post("/user")]
pub async fn register(
pool: web::Data<db::PgPool>,
register_user: web::Json<user::RegisterUserDto>,
) -> Result<HttpResponse, ApiError> {
let conn = db::get_conn(&pool.get_ref())?;

let result = web::block(move || {
    service::registration::register_user(
        &conn,
        InsertableUser {
            username: register_user.username.clone(),
            password: register_user.password.clone(),
            email: register_user.password.clone(),
            birth_date: register_user.birth_date.clone(),
        },
    )
})
.await;

return match result {
    Ok(u) => Ok(HttpResponse::Ok().json(u)),
    Err(e) => match e {
        BlockingError::Error(e2) => match e2 {
            RegisterError::UsernameAlreadyExists(username) => Err(ApiError::new(
                409,
                format!("Username {} already exists", username),
                1001,
                None,
            )),
            RegisterError::GenericDatabaseError(_) => Err(ApiError::new(
                500,
                StatusCode::INTERNAL_SERVER_ERROR
                    .canonical_reason()
                    .unwrap_or("")
                    .to_string(),
                5000,
                None,
            )),
            RegisterError::HashError(_) => Err(ApiError::new(
                500,
                StatusCode::INTERNAL_SERVER_ERROR
                    .canonical_reason()
                    .unwrap_or("")
                    .to_string(),
                5001,
                None,
            )),
        },
        BlockingError::Canceled => Err(ApiError::new(
            500,
            StatusCode::INTERNAL_SERVER_ERROR
                .canonical_reason()
                .unwrap_or("")
                .to_string(),
            5002,
            None,
        )),
    },
};

}

Is there any established pattern to do that? Implementing the From for ApiError trait also does not help much. Should the service layer return HttpResponses in form of my ApiErrors directly, instead of using an own error enum? That would reduce the match arms in the controller. Then I could only call one service method per controller method though.

I think the only thing that could work is that all service methods return their errorEnum, but somehow they can share common errors. And there is a mapping from each error to an ApiError Response, which is called in the controller if the flow is finished at that point.

Aucun commentaire:

Enregistrer un commentaire