dogy_backend_api/
error.rs

1//! This module contains the main error handling for all routes and axum handlers.
2//!
3//! This follows the rust-10x production code convention for handling errors.
4//! See [this video](https://www.youtube.com/watch?v=j-VQCYP7wyw) to know more about it.
5//!
6//! Moreover, this module contains error handling for both server errors and client errors.
7
8use std::sync::Arc;
9
10use crate::{
11    middleware::auth,
12    service::{assistant::daily_challenges, users},
13};
14use axum::{extract::rejection::JsonRejection, http::StatusCode, response::IntoResponse, Json};
15use derive_more::From;
16use serde::Serialize;
17use serde_with::{serde_as, DisplayFromStr};
18use tracing::debug;
19use uuid::Uuid;
20
21/// Boilerplate Result Type from rust-10x style convention. This result type is used by all axum
22/// handlers.
23pub type Result<T> = core::result::Result<T, Error>;
24
25/// This type is used when you have a JSON request body as it can potentially throw
26/// [`JsonRejection`], which is not handled by [`serde_json`].
27///
28/// See this example when using in a handler:
29/// ```rust
30/// async fn update_user_base(payload: PayloadJson<UserUpdate>) -> Result<Json<UserUpdate>> {
31///     Json(user) = payload?;
32///     println!("User ID: {user.id}");
33/// }
34/// ```
35pub type PayloadJson<T> = core::result::Result<Json<T>, JsonRejection>;
36
37/// Main Error Types for all axum handlers.
38///
39/// This contains all of the error types from all other modules.
40/// In particular, the [`From`] trait is used to cast the error types from other modules to this.
41/// Moreover, This handles the following:
42/// - Error types from [serde_json][`serde_json::Error`], if an axum handler fails to serialize to
43/// JSON as a response.
44/// - JSON Rejection errors from [axum][`axum::extract::rejection::JsonRejection`], if an axum handler
45/// fails to deserialize the request body to JSON. This occurs if the consumer of the API sends a
46/// malformed/incorrect format for the request body.
47/// - Error types from [auth][`auth::Error`].
48/// - Error types from [users][`users::Error`].
49///
50#[serde_as]
51#[derive(Debug, Serialize, From, strum_macros::AsRefStr)]
52#[serde(tag = "type", content = "data")]
53pub enum Error {
54    // Internals
55    /// This error will occur if an environment variable imported using
56    /// `get_env()` is missing when the server starts up.
57    /// If you want to import env variables with default value such as `PORT`, you can use
58    /// `get_env_opt()` instead.
59    ConfigMissingEnv(&'static str),
60
61    // Modules
62    /// Errors from [`auth::Error`]
63    #[from]
64    Auth(auth::Error),
65
66    /// Errors from [`auth::Error`]
67    #[from]
68    User(users::Error),
69
70    #[from]
71    DailyChallenge(daily_challenges::Error),
72
73    /// Errors from [`serde_json::Error`]
74    #[from]
75    SerdeJson(#[serde_as(as = "DisplayFromStr")] serde_json::Error),
76
77    /// This error will occur if a request body is not valid JSON.
78    #[from]
79    JsonRejection(#[serde_as(as = "DisplayFromStr")] JsonRejection),
80}
81
82/// This is the error type that is returned to the client.
83///
84/// This error type can only be retrieved after mapping a server error to a client error.
85#[derive(Debug, Serialize, strum_macros::AsRefStr)]
86#[serde(tag = "code", content = "details")]
87#[allow(non_camel_case_types)]
88pub enum ClientError {
89    // Auth
90    /// This error will occur if the request does not contain an `Authorization` header.
91    MISSING_AUTH_HEADER,
92
93    /// This error will occur if the request does not contain a `Bearer` prefix in the
94    /// `Authorization` header.
95    NO_BEARER_PREFIX,
96
97    /// This error will occur if the token in the `Authorization` header is invalid.
98    INVALID_CREDENTIALS,
99
100    /// This error will occur if a server error is not mapped to client error.
101    SERVICE_ERROR,
102
103    // User
104    /// This error will occur if a user attempts to create a user that already exists in the
105    /// database.
106    USER_ALREADY_EXISTS,
107
108    /// This error will occur if an authenticated user is not found in the database.
109    USER_NOT_FOUND {
110        user_id: String,
111    },
112
113    // Invalid Request Body
114    /// This error will occur if a request body is not valid JSON or it did not meet the
115    /// requirements for serialization.
116    INVALID_REQUEST_BODY(String),
117
118    // Daily Challenge
119    DAILY_CHALLENGE_ALREADY_COMPLETED {
120        challenge_id: Uuid,
121    },
122}
123
124impl IntoResponse for Error {
125    /// Initializes errors to client errors with `INTERNAL_SERVER_ERROR` and injects the server error into the
126    /// request through the use of extensions.
127    ///
128    /// The injected server error will be retrieved later on and will be mapped to a proper client
129    /// error.
130    fn into_response(self) -> axum::response::Response {
131        debug!("{self:?}");
132
133        let mut response = StatusCode::INTERNAL_SERVER_ERROR.into_response();
134
135        response.extensions_mut().insert(Arc::new(self));
136        response
137    }
138}
139
140impl Error {
141    /// Maps the injected server error of type [`Error`] to a client error [`ClientError`].
142    pub fn client_status_error(&self) -> (StatusCode, ClientError) {
143        match self {
144            Error::Auth(auth::Error::MissingAuthHeader) => {
145                (StatusCode::UNAUTHORIZED, ClientError::MISSING_AUTH_HEADER)
146            }
147            Error::Auth(auth::Error::NoBearerPrefix) => {
148                (StatusCode::UNAUTHORIZED, ClientError::NO_BEARER_PREFIX)
149            }
150            Error::Auth(auth::Error::InvalidToken) => {
151                (StatusCode::UNAUTHORIZED, ClientError::INVALID_CREDENTIALS)
152            }
153            Error::Auth(auth::Error::UserNotFound { user_id }) => (
154                StatusCode::NOT_FOUND,
155                ClientError::USER_NOT_FOUND {
156                    user_id: user_id.to_string(),
157                },
158            ),
159            Error::User(users::Error::UserAlreadyExists) => {
160                (StatusCode::CONFLICT, ClientError::USER_ALREADY_EXISTS)
161            }
162            Error::JsonRejection(req_body) => (
163                StatusCode::UNPROCESSABLE_ENTITY,
164                ClientError::INVALID_REQUEST_BODY(req_body.to_string()),
165            ),
166            Error::DailyChallenge(daily_challenges::Error::ChallengeAlreadyCompleted {
167                challenge_id,
168            }) => (
169                StatusCode::CONFLICT,
170                ClientError::DAILY_CHALLENGE_ALREADY_COMPLETED {
171                    challenge_id: *challenge_id,
172                },
173            ),
174            _ => (
175                StatusCode::INTERNAL_SERVER_ERROR,
176                ClientError::SERVICE_ERROR,
177            ),
178        }
179    }
180}
181
182// Boilerplate for Errors
183impl std::fmt::Display for Error {
184    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> core::result::Result<(), core::fmt::Error> {
185        write!(f, "{self:?}")
186    }
187}
188
189impl std::error::Error for Error {}