this repo has no description
1use axum::{
2 Json,
3 http::StatusCode,
4 response::{IntoResponse, Response},
5};
6use serde::Serialize;
7
8#[derive(Debug, Serialize)]
9struct ErrorBody {
10 error: &'static str,
11 #[serde(skip_serializing_if = "Option::is_none")]
12 message: Option<String>,
13}
14
15#[derive(Debug)]
16pub enum ApiError {
17 InternalError,
18 AuthenticationRequired,
19 AuthenticationFailed,
20 AuthenticationFailedMsg(String),
21 InvalidRequest(String),
22 InvalidToken,
23 ExpiredToken,
24 ExpiredTokenMsg(String),
25 TokenRequired,
26 AccountDeactivated,
27 AccountTakedown,
28 AccountNotFound,
29 RepoNotFound,
30 RepoNotFoundMsg(String),
31 RecordNotFound,
32 BlobNotFound,
33 InvalidHandle,
34 HandleNotAvailable,
35 HandleTaken,
36 InvalidEmail,
37 EmailTaken,
38 InvalidInviteCode,
39 DuplicateCreate,
40 DuplicateAppPassword,
41 AppPasswordNotFound,
42 InvalidSwap,
43 Forbidden,
44 InvitesDisabled,
45 DatabaseError,
46 UpstreamFailure,
47 UpstreamTimeout,
48 UpstreamUnavailable(String),
49 UpstreamError { status: u16, error: Option<String>, message: Option<String> },
50}
51
52impl ApiError {
53 fn status_code(&self) -> StatusCode {
54 match self {
55 Self::InternalError | Self::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR,
56 Self::UpstreamFailure | Self::UpstreamUnavailable(_) => StatusCode::BAD_GATEWAY,
57 Self::UpstreamTimeout => StatusCode::GATEWAY_TIMEOUT,
58 Self::UpstreamError { status, .. } => {
59 StatusCode::from_u16(*status).unwrap_or(StatusCode::BAD_GATEWAY)
60 }
61 Self::AuthenticationRequired
62 | Self::AuthenticationFailed
63 | Self::AuthenticationFailedMsg(_)
64 | Self::InvalidToken
65 | Self::ExpiredToken
66 | Self::ExpiredTokenMsg(_)
67 | Self::TokenRequired
68 | Self::AccountDeactivated
69 | Self::AccountTakedown => StatusCode::UNAUTHORIZED,
70 Self::Forbidden | Self::InvitesDisabled => StatusCode::FORBIDDEN,
71 Self::AccountNotFound
72 | Self::RepoNotFound
73 | Self::RepoNotFoundMsg(_)
74 | Self::RecordNotFound
75 | Self::BlobNotFound
76 | Self::AppPasswordNotFound => StatusCode::NOT_FOUND,
77 Self::InvalidRequest(_)
78 | Self::InvalidHandle
79 | Self::HandleNotAvailable
80 | Self::HandleTaken
81 | Self::InvalidEmail
82 | Self::EmailTaken
83 | Self::InvalidInviteCode
84 | Self::DuplicateCreate
85 | Self::DuplicateAppPassword
86 | Self::InvalidSwap => StatusCode::BAD_REQUEST,
87 }
88 }
89 fn error_name(&self) -> &'static str {
90 match self {
91 Self::InternalError | Self::DatabaseError => "InternalError",
92 Self::UpstreamFailure | Self::UpstreamUnavailable(_) => "UpstreamFailure",
93 Self::UpstreamTimeout => "UpstreamTimeout",
94 Self::UpstreamError { error, .. } => {
95 if let Some(e) = error {
96 return Box::leak(e.clone().into_boxed_str());
97 }
98 "UpstreamError"
99 }
100 Self::AuthenticationRequired => "AuthenticationRequired",
101 Self::AuthenticationFailed | Self::AuthenticationFailedMsg(_) => "AuthenticationFailed",
102 Self::InvalidToken => "InvalidToken",
103 Self::ExpiredToken | Self::ExpiredTokenMsg(_) => "ExpiredToken",
104 Self::TokenRequired => "TokenRequired",
105 Self::AccountDeactivated => "AccountDeactivated",
106 Self::AccountTakedown => "AccountTakedown",
107 Self::Forbidden => "Forbidden",
108 Self::InvitesDisabled => "InvitesDisabled",
109 Self::AccountNotFound => "AccountNotFound",
110 Self::RepoNotFound | Self::RepoNotFoundMsg(_) => "RepoNotFound",
111 Self::RecordNotFound => "RecordNotFound",
112 Self::BlobNotFound => "BlobNotFound",
113 Self::AppPasswordNotFound => "AppPasswordNotFound",
114 Self::InvalidRequest(_) => "InvalidRequest",
115 Self::InvalidHandle => "InvalidHandle",
116 Self::HandleNotAvailable => "HandleNotAvailable",
117 Self::HandleTaken => "HandleTaken",
118 Self::InvalidEmail => "InvalidEmail",
119 Self::EmailTaken => "EmailTaken",
120 Self::InvalidInviteCode => "InvalidInviteCode",
121 Self::DuplicateCreate => "DuplicateCreate",
122 Self::DuplicateAppPassword => "DuplicateAppPassword",
123 Self::InvalidSwap => "InvalidSwap",
124 }
125 }
126 fn message(&self) -> Option<String> {
127 match self {
128 Self::AuthenticationFailedMsg(msg)
129 | Self::ExpiredTokenMsg(msg)
130 | Self::InvalidRequest(msg)
131 | Self::RepoNotFoundMsg(msg)
132 | Self::UpstreamUnavailable(msg) => Some(msg.clone()),
133 Self::UpstreamError { message, .. } => message.clone(),
134 Self::UpstreamTimeout => Some("Upstream service timed out".to_string()),
135 _ => None,
136 }
137 }
138 pub fn from_upstream_response(
139 status: u16,
140 body: &[u8],
141 ) -> Self {
142 if let Ok(parsed) = serde_json::from_slice::<serde_json::Value>(body) {
143 let error = parsed.get("error").and_then(|v| v.as_str()).map(String::from);
144 let message = parsed.get("message").and_then(|v| v.as_str()).map(String::from);
145 return Self::UpstreamError { status, error, message };
146 }
147 Self::UpstreamError { status, error: None, message: None }
148 }
149}
150
151impl IntoResponse for ApiError {
152 fn into_response(self) -> Response {
153 let body = ErrorBody {
154 error: self.error_name(),
155 message: self.message(),
156 };
157 (self.status_code(), Json(body)).into_response()
158 }
159}
160
161impl From<sqlx::Error> for ApiError {
162 fn from(e: sqlx::Error) -> Self {
163 tracing::error!("Database error: {:?}", e);
164 Self::DatabaseError
165 }
166}
167
168impl From<crate::auth::TokenValidationError> for ApiError {
169 fn from(e: crate::auth::TokenValidationError) -> Self {
170 match e {
171 crate::auth::TokenValidationError::AccountDeactivated => Self::AccountDeactivated,
172 crate::auth::TokenValidationError::AccountTakedown => Self::AccountTakedown,
173 crate::auth::TokenValidationError::KeyDecryptionFailed => Self::InternalError,
174 crate::auth::TokenValidationError::AuthenticationFailed => Self::AuthenticationFailed,
175 }
176 }
177}
178
179impl From<crate::util::DbLookupError> for ApiError {
180 fn from(e: crate::util::DbLookupError) -> Self {
181 match e {
182 crate::util::DbLookupError::NotFound => Self::AccountNotFound,
183 crate::util::DbLookupError::DatabaseError(db_err) => {
184 tracing::error!("Database error: {:?}", db_err);
185 Self::DatabaseError
186 }
187 }
188 }
189}