kellnr_error/
api_error.rs1use 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 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 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}