From 2c58796b335880c3f1a4fd8a79f333bf69129612 Mon Sep 17 00:00:00 2001 From: "Dustin C. Hatch" Date: Sat, 15 Jan 2022 21:44:11 -0600 Subject: [PATCH] backend: web: Refactor error handling Trying to make errors more ergonomic and idiomatic. Requiring the handler functions to return a two-tuple with the HTTP status and the `Json` guard would have gotten cumbersome very quickly. To make it simpler, there are two new error enumerations: * `Error` is a general-purpose type that can be returned from the service layer. There is a variant for each type of problem that can occur. * `ApiError` is specifically for the API layer, and implements the Rocket `Responder` trait. This is where mapping errors to HTTP status codes takes place. The `From` trait is implemented for `ApiError`, allowing `Error` values returned from the service layer to be passed directly back from request handlers using the `?` operator. Additionally, the `Error` enum implements `From` for JSON-RPC errors, essentially mapping any unhandled error from the RPC layer to HTTP 503 Service Unavailable, indicating a problem communicating with the daemon. --- backend/src/server/context.rs | 14 +++-------- backend/src/server/error.rs | 40 ++++++++++++++++++++++++++----- backend/src/server/routes/auth.rs | 8 +++---- 3 files changed, 41 insertions(+), 21 deletions(-) diff --git a/backend/src/server/context.rs b/backend/src/server/context.rs index 95973d5..3715d33 100644 --- a/backend/src/server/context.rs +++ b/backend/src/server/context.rs @@ -1,9 +1,7 @@ use super::config::Config; use crate::rpc::gen_client::Client; -use crate::server::error::{ApiError, ErrorResponse}; +use crate::server::error::Error; use jsonrpc_core_client::transports::ipc::connect; -use rocket::http::Status as HttpStatus; -use rocket::serde::json::Json; pub struct Context { config: Config, @@ -18,16 +16,10 @@ impl Context { Self { config } } - pub async fn client(&self) -> Result { + pub async fn client(&self) -> Result { match connect(&self.config.socket).await { Ok(client) => Ok(client), - Err(e) => Err(( - HttpStatus::ServiceUnavailable, - Json(ErrorResponse::new(format!( - "Cannot connect to daemon: {}", - e - ))), - )), + Err(e) => Err(format!("Cannot connect to daemon: {}", e)), } } diff --git a/backend/src/server/error.rs b/backend/src/server/error.rs index 0877edc..f94acf7 100644 --- a/backend/src/server/error.rs +++ b/backend/src/server/error.rs @@ -1,14 +1,13 @@ -use rocket::http::Status as HttpStatus; +use jsonrpc_core_client::RpcError; +use rocket::response::Responder; use rocket::serde::json::Json; use rocket::serde::Serialize; -#[derive(Serialize)] +#[derive(Debug, Serialize)] pub struct ErrorResponse { pub error: String, } -pub type ApiError = (HttpStatus, Json); - impl ErrorResponse { pub fn new>(message: S) -> Self { Self { @@ -17,6 +16,35 @@ impl ErrorResponse { } } -pub fn error>(status: HttpStatus, message: S) -> ApiError { - (status, Json(ErrorResponse::new(message))) +#[derive(Debug, Responder)] +pub enum ApiError { + #[response(status = 503, content_type = "json")] + ServiceUnavailable(Json), + + #[response(status = 401, content_type = "json")] + AuthError(Json), +} + +pub enum Error { + ServiceUnavailable(String), + AuthError(String), +} + +impl From for ApiError { + fn from(error: Error) -> Self { + match error { + Error::ServiceUnavailable(e) => { + Self::ServiceUnavailable(Json(ErrorResponse::new(e))) + } + Error::AuthError(e) => { + Self::AuthError(Json(ErrorResponse::new(e))) + } + } + } +} + +impl From for Error { + fn from(error: RpcError) -> Self { + Self::ServiceUnavailable(error.to_string()) + } } diff --git a/backend/src/server/routes/auth.rs b/backend/src/server/routes/auth.rs index 56a40df..9191c6c 100644 --- a/backend/src/server/routes/auth.rs +++ b/backend/src/server/routes/auth.rs @@ -1,8 +1,8 @@ use super::super::context::Context; -use super::super::error::{error, ApiError}; +use super::super::error::{ApiError, Error}; use crate::models::auth::LoginResponse; use rocket::form::{Form, FromForm}; -use rocket::http::{Cookie, CookieJar, Status}; +use rocket::http::{Cookie, CookieJar}; use rocket::serde::json::Json; use rocket::serde::Serialize; use rocket::State; @@ -31,8 +31,8 @@ pub async fn login( .await { Ok(_) => {} - Err(e) => return Err(error(Status::Unauthorized, e.to_string())), - } + Err(e) => return Err(Error::AuthError(e.to_string()).into()), + }; let cookie = AuthCookie { username: form.username.into(), };