Skip to main content

kellnr_error/
api_error.rs

1use std::fmt::Display;
2
3use axum::Json;
4use axum::http::StatusCode;
5use axum::response::{IntoResponse, Response};
6use kellnr_common::original_name::NameError;
7use kellnr_common::version::VersionError;
8use kellnr_db::error::DbError;
9use kellnr_storage::storage_error::StorageError;
10use serde::{Deserialize, Serialize};
11use zip::result::ZipError;
12
13pub type ApiResult<T> = Result<T, ApiError>;
14
15pub struct ApiError {
16    status: StatusCode,
17    details: ErrorDetails,
18}
19
20#[derive(Serialize, Deserialize, Debug)]
21pub struct ErrorDetails {
22    pub errors: Vec<ErrorDetail>,
23}
24
25impl From<String> for ErrorDetails {
26    fn from(e: String) -> Self {
27        let detail = ErrorDetail { detail: e };
28        Self {
29            errors: vec![detail],
30        }
31    }
32}
33
34#[derive(Serialize, Deserialize, Debug)]
35pub struct ErrorDetail {
36    pub detail: String,
37}
38
39impl ApiError {
40    pub fn new(msg: &str, error: &str, status: StatusCode) -> Self {
41        let e = if error.is_empty() {
42            format!("ERROR: {msg}")
43        } else {
44            format!("ERROR: {msg} -> {error}")
45        };
46        Self {
47            status,
48            details: ErrorDetails::from(e),
49        }
50    }
51
52    fn from_str(e: &str, status: StatusCode) -> Self {
53        Self {
54            status,
55            details: ErrorDetails::from(format!("ERROR: {e}")),
56        }
57    }
58
59    pub fn from_err(e: &dyn std::error::Error, status: StatusCode) -> Self {
60        let e = format!("ERROR: {e}");
61        Self {
62            status,
63            details: ErrorDetails::from(e),
64        }
65    }
66
67    /// Log `e` server-side and return a generic 500 error that does not leak
68    /// internal details like SQL statements or storage paths to the client.
69    pub fn internal(e: &dyn std::error::Error) -> Self {
70        tracing::error!("Internal server error: {e}");
71        Self::from_str("Internal server error", StatusCode::INTERNAL_SERVER_ERROR)
72    }
73}
74
75impl IntoResponse for ApiError {
76    fn into_response(self) -> Response {
77        (self.status, Json(self.details)).into_response()
78    }
79}
80
81impl From<NameError> for ApiError {
82    fn from(e: NameError) -> Self {
83        ApiError::from_err(&e, StatusCode::BAD_REQUEST)
84    }
85}
86
87impl From<VersionError> for ApiError {
88    fn from(e: VersionError) -> Self {
89        ApiError::from_err(&e, StatusCode::BAD_REQUEST)
90    }
91}
92
93impl From<std::io::Error> for ApiError {
94    fn from(e: std::io::Error) -> Self {
95        ApiError::internal(&e)
96    }
97}
98
99impl From<ZipError> for ApiError {
100    fn from(e: ZipError) -> Self {
101        match e {
102            ZipError::Io(e) => ApiError::internal(&e),
103            ZipError::InvalidArchive(s) => ApiError::from_str(&s, StatusCode::BAD_REQUEST),
104            ZipError::UnsupportedArchive(s) => ApiError::from_str(s, StatusCode::BAD_REQUEST),
105            ZipError::FileNotFound => {
106                ApiError::from_str("Zip archive not found", StatusCode::NOT_FOUND)
107            }
108            _ => ApiError::from_str("Unknown zip error", StatusCode::INTERNAL_SERVER_ERROR),
109        }
110    }
111}
112
113impl Display for ApiError {
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        write!(f, "{}", self.details.errors[0].detail)
116    }
117}
118
119impl From<DbError> for ApiError {
120    fn from(e: DbError) -> Self {
121        match e {
122            DbError::NotOwner(_) => ApiError::from_err(&e, StatusCode::FORBIDDEN),
123            DbError::CrateNameConflict(_, _) => ApiError::from_err(&e, StatusCode::BAD_REQUEST),
124            // These only carry user supplied names and are useful to the client.
125            DbError::PasswordMismatch
126            | DbError::UserNotFound(_)
127            | DbError::GroupNotFound(_)
128            | DbError::OwnerNotFound(_)
129            | DbError::CrateNotFound(_)
130            | DbError::CrateNotFoundWithId(_)
131            | DbError::CrateMetaNotFound(_, _)
132            | DbError::CrateIndexNotFound(_, _)
133            | DbError::InvalidCrateName(_)
134            | DbError::InvalidVersion(_)
135            | DbError::TokenNotFound
136            | DbError::SessionNotFound
137            | DbError::WebhookNotFound
138            | DbError::InvalidWebhookEvent(_)
139            | DbError::InvalidId(_) => ApiError::from_err(&e, StatusCode::INTERNAL_SERVER_ERROR),
140            _ => ApiError::internal(&e),
141        }
142    }
143}
144
145impl From<StorageError> for ApiError {
146    fn from(e: StorageError) -> Self {
147        match e {
148            StorageError::CrateExists(_, _) | StorageError::ToolchainArchiveExists { .. } => {
149                ApiError::from_err(&e, StatusCode::INTERNAL_SERVER_ERROR)
150            }
151            _ => ApiError::internal(&e),
152        }
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn database_errors_are_not_leaked() {
162        let e = ApiError::from(DbError::PostgresError(sea_orm::DbErr::Custom(
163            "SELECT secret FROM users".to_string(),
164        )));
165
166        assert_eq!(StatusCode::INTERNAL_SERVER_ERROR, e.status);
167        assert_eq!("ERROR: Internal server error", e.to_string());
168    }
169
170    #[test]
171    fn user_facing_database_errors_stay_descriptive() {
172        let e = ApiError::from(DbError::UserNotFound("bob".to_string()));
173
174        assert_eq!("ERROR: User not found: bob", e.to_string());
175    }
176
177    #[test]
178    fn storage_and_io_errors_are_not_leaked() {
179        let storage = ApiError::from(StorageError::GenericError("/var/lib/kellnr".to_string()));
180        let io = ApiError::from(std::io::Error::other("/var/lib/kellnr"));
181
182        assert_eq!("ERROR: Internal server error", storage.to_string());
183        assert_eq!("ERROR: Internal server error", io.to_string());
184    }
185
186    #[test]
187    fn crate_exists_storage_error_stays_descriptive() {
188        let e = ApiError::from(StorageError::CrateExists(
189            "foo".to_string(),
190            "1.0.0".to_string(),
191        ));
192
193        assert_eq!(
194            "ERROR: Crate with version already exists: foo-1.0.0",
195            e.to_string()
196        );
197    }
198}