1use std::fmt::Display;
2use std::time::Duration;
3
4use axum::Json;
5use axum::http::{StatusCode, header};
6use axum::response::{IntoResponse, Response};
7use kellnr_common::account_name::AccountNameError;
8use kellnr_common::original_name::NameError;
9use kellnr_common::version::VersionError;
10use kellnr_db::error::DbError;
11use kellnr_storage::storage_error::StorageError;
12use serde::{Deserialize, Serialize};
13use utoipa::ToSchema;
14use zip::result::ZipError;
15
16pub type ApiResult<T> = Result<T, ApiError>;
17
18#[derive(Debug)]
21pub struct ApiError {
22 status: StatusCode,
23 details: ErrorDetails,
24 retry_after: Option<Duration>,
26}
27
28#[derive(Serialize, Deserialize, Debug, ToSchema)]
29pub struct ErrorDetails {
30 pub errors: Vec<ErrorDetail>,
31}
32
33impl From<String> for ErrorDetails {
34 fn from(e: String) -> Self {
35 let detail = ErrorDetail { detail: e };
36 Self {
37 errors: vec![detail],
38 }
39 }
40}
41
42#[derive(Serialize, Deserialize, Debug, ToSchema)]
43pub struct ErrorDetail {
44 pub detail: String,
45}
46
47impl ApiError {
48 pub fn new(msg: &str, status: StatusCode) -> Self {
50 Self::from_str(msg, status)
51 }
52
53 fn from_str(e: &str, status: StatusCode) -> Self {
54 Self {
55 status,
56 details: ErrorDetails::from(format!("ERROR: {e}")),
57 retry_after: None,
58 }
59 }
60
61 pub fn bad_request(msg: impl Display) -> Self {
62 Self::from_str(&msg.to_string(), StatusCode::BAD_REQUEST)
63 }
64
65 pub fn unauthorized(msg: impl Display) -> Self {
66 Self::from_str(&msg.to_string(), StatusCode::UNAUTHORIZED)
67 }
68
69 pub fn forbidden(msg: impl Display) -> Self {
70 Self::from_str(&msg.to_string(), StatusCode::FORBIDDEN)
71 }
72
73 pub fn not_found(msg: impl Display) -> Self {
74 Self::from_str(&msg.to_string(), StatusCode::NOT_FOUND)
75 }
76
77 pub fn conflict(msg: impl Display) -> Self {
78 Self::from_str(&msg.to_string(), StatusCode::CONFLICT)
79 }
80
81 pub fn service_unavailable(msg: impl Display, retry_after: Duration) -> Self {
83 Self {
84 retry_after: Some(retry_after),
85 ..Self::from_str(&msg.to_string(), StatusCode::SERVICE_UNAVAILABLE)
86 }
87 }
88
89 pub fn status(&self) -> StatusCode {
90 self.status
91 }
92
93 pub fn from_err(e: &dyn std::error::Error, status: StatusCode) -> Self {
94 let e = format!("ERROR: {e}");
95 Self {
96 status,
97 details: ErrorDetails::from(e),
98 retry_after: None,
99 }
100 }
101
102 pub fn internal(e: &dyn std::error::Error) -> Self {
105 tracing::error!("Internal server error: {e}");
106 Self::from_str("Internal server error", StatusCode::INTERNAL_SERVER_ERROR)
107 }
108}
109
110impl IntoResponse for ApiError {
111 fn into_response(self) -> Response {
112 let mut response = (self.status, Json(self.details)).into_response();
113 if let Some(retry_after) = self.retry_after {
114 response
115 .headers_mut()
116 .insert(header::RETRY_AFTER, retry_after.as_secs().into());
117 }
118 response
119 }
120}
121
122pub struct PlainTextError {
126 status: StatusCode,
127 message: String,
128}
129
130impl PlainTextError {
131 pub fn new(status: StatusCode, message: impl Into<String>) -> Self {
132 Self {
133 status,
134 message: message.into(),
135 }
136 }
137}
138
139#[derive(Clone, Copy)]
141pub(crate) struct KeepPlainText;
142
143impl IntoResponse for PlainTextError {
144 fn into_response(self) -> Response {
145 let mut response = (self.status, self.message).into_response();
146 response.extensions_mut().insert(KeepPlainText);
147 response
148 }
149}
150
151impl From<StatusCode> for ApiError {
153 fn from(status: StatusCode) -> Self {
154 Self::from_str(status.canonical_reason().unwrap_or("Error"), status)
155 }
156}
157
158impl From<AccountNameError> for ApiError {
159 fn from(e: AccountNameError) -> Self {
160 ApiError::from_err(&e, StatusCode::BAD_REQUEST)
161 }
162}
163
164impl From<NameError> for ApiError {
165 fn from(e: NameError) -> Self {
166 ApiError::from_err(&e, StatusCode::BAD_REQUEST)
167 }
168}
169
170impl From<VersionError> for ApiError {
171 fn from(e: VersionError) -> Self {
172 ApiError::from_err(&e, StatusCode::BAD_REQUEST)
173 }
174}
175
176impl From<std::io::Error> for ApiError {
177 fn from(e: std::io::Error) -> Self {
178 ApiError::internal(&e)
179 }
180}
181
182impl From<ZipError> for ApiError {
183 fn from(e: ZipError) -> Self {
184 match e {
185 ZipError::Io(e) => ApiError::internal(&e),
186 ZipError::InvalidArchive(s) => ApiError::from_str(&s, StatusCode::BAD_REQUEST),
187 ZipError::UnsupportedArchive(s) => ApiError::from_str(s, StatusCode::BAD_REQUEST),
188 ZipError::FileNotFound => {
189 ApiError::from_str("Zip archive not found", StatusCode::NOT_FOUND)
190 }
191 _ => ApiError::from_str("Unknown zip error", StatusCode::INTERNAL_SERVER_ERROR),
192 }
193 }
194}
195
196impl Display for ApiError {
197 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
198 write!(f, "{}", self.details.errors[0].detail)
199 }
200}
201
202impl From<DbError> for ApiError {
203 fn from(e: DbError) -> Self {
204 match e {
206 DbError::NotOwner(_) => ApiError::from_err(&e, StatusCode::FORBIDDEN),
207 DbError::UserNotFound(_)
208 | DbError::GroupNotFound(_)
209 | DbError::OwnerNotFound(_)
210 | DbError::CrateNotFound(_)
211 | DbError::CrateNotFoundWithId(_)
212 | DbError::CrateMetaNotFound(_, _)
213 | DbError::CrateIndexNotFound(_, _)
214 | DbError::TokenNotFound
215 | DbError::SessionNotFound
216 | DbError::WebhookNotFound
217 | DbError::ToolchainNotFound(_, _) => ApiError::from_err(&e, StatusCode::NOT_FOUND),
218 DbError::PasswordMismatch
219 | DbError::CrateNameConflict(_, _)
220 | DbError::CrateNameMismatch(_, _)
221 | DbError::InvalidCrateName(_)
222 | DbError::InvalidVersion(_)
223 | DbError::InvalidWebhookEvent(_)
224 | DbError::InvalidId(_) => ApiError::from_err(&e, StatusCode::BAD_REQUEST),
225 DbError::CrateVersionExists(_, _) | DbError::LastOwner => {
226 ApiError::from_err(&e, StatusCode::CONFLICT)
227 }
228 _ if e.is_unique_violation() => {
229 tracing::warn!("Conflict with an existing entry: {e}");
230 ApiError::from_str("The entry already exists", StatusCode::CONFLICT)
231 }
232 _ => ApiError::internal(&e),
233 }
234 }
235}
236
237impl From<StorageError> for ApiError {
238 fn from(e: StorageError) -> Self {
239 match e {
240 StorageError::CrateExists(_, _) | StorageError::ToolchainArchiveExists { .. } => {
241 ApiError::from_err(&e, StatusCode::CONFLICT)
242 }
243 _ => ApiError::internal(&e),
244 }
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[test]
253 fn new_prefixes_the_message() {
254 let e = ApiError::new("Unauthorized", StatusCode::UNAUTHORIZED);
255
256 assert_eq!(StatusCode::UNAUTHORIZED, e.status);
257 assert_eq!(1, e.details.errors.len());
258 assert_eq!("ERROR: Unauthorized", e.to_string());
259 }
260
261 #[test]
262 fn service_unavailable_sends_retry_after() {
263 let response =
264 ApiError::service_unavailable("down", Duration::from_secs(30)).into_response();
265
266 assert_eq!(StatusCode::SERVICE_UNAVAILABLE, response.status());
267 assert_eq!("30", response.headers()[header::RETRY_AFTER]);
268 }
269
270 #[test]
271 fn database_errors_are_not_leaked() {
272 let e = ApiError::from(DbError::PostgresError(sea_orm::DbErr::Custom(
273 "SELECT secret FROM users".to_string(),
274 )));
275
276 assert_eq!(StatusCode::INTERNAL_SERVER_ERROR, e.status);
277 assert_eq!("ERROR: Internal server error", e.to_string());
278 }
279
280 #[test]
281 fn user_facing_database_errors_stay_descriptive() {
282 let e = ApiError::from(DbError::UserNotFound("bob".to_string()));
283
284 assert_eq!("ERROR: User not found: bob", e.to_string());
285 }
286
287 #[test]
288 fn missing_entries_are_not_found() {
289 for e in [
290 DbError::CrateNotFound("foo".to_string()),
291 DbError::OwnerNotFound("bob".to_string()),
292 DbError::TokenNotFound,
293 DbError::ToolchainNotFound("stable".to_string(), "x86_64".to_string()),
294 ] {
295 assert_eq!(StatusCode::NOT_FOUND, ApiError::from(e).status);
296 }
297 }
298
299 #[test]
300 fn invalid_input_is_a_bad_request() {
301 for e in [
302 DbError::InvalidCrateName("f o o".to_string()),
303 DbError::InvalidVersion("x".to_string()),
304 DbError::InvalidId("x".to_string()),
305 DbError::PasswordMismatch,
306 ] {
307 assert_eq!(StatusCode::BAD_REQUEST, ApiError::from(e).status);
308 }
309 }
310
311 #[test]
312 fn storage_and_io_errors_are_not_leaked() {
313 let storage = ApiError::from(StorageError::GenericError("/var/lib/kellnr".to_string()));
314 let io = ApiError::from(std::io::Error::other("/var/lib/kellnr"));
315
316 assert_eq!("ERROR: Internal server error", storage.to_string());
317 assert_eq!("ERROR: Internal server error", io.to_string());
318 }
319
320 #[test]
321 fn crate_exists_storage_error_stays_descriptive() {
322 let e = ApiError::from(StorageError::CrateExists(
323 "foo".to_string(),
324 "1.0.0".to_string(),
325 ));
326
327 assert_eq!(StatusCode::CONFLICT, e.status);
328 assert_eq!(
329 "ERROR: Crate with version already exists: foo-1.0.0",
330 e.to_string()
331 );
332 }
333}