1use std::borrow::Cow;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
7#[serde(rename_all = "snake_case")]
8pub enum ServiceErrorCode {
9 BadRequest,
10 ValidationFailed,
11 Unauthorized,
12 Forbidden,
13 NotFound,
14 Conflict,
15 TooManyRequests,
16 PayloadTooLarge,
17 InternalError,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
22pub struct ErrorDetail {
23 #[serde(skip_serializing_if = "Option::is_none")]
24 pub field: Option<String>,
25 pub message: String,
26}
27
28#[derive(Debug, thiserror::Error)]
29pub enum ServiceError {
31 #[error("database error: {0}")]
32 Database(#[from] sqlx::Error),
33 #[error("not found")]
34 NotFound,
35 #[error("conflict")]
36 Conflict,
37 #[error("{message}")]
38 ConflictDetails {
39 message: String,
40 details: Vec<ErrorDetail>,
41 },
42 #[error("bad request: {0}")]
43 BadRequest(String),
44 #[error("too many requests: {0}")]
45 TooManyRequests(String),
46 #[error("unauthorized")]
47 Unauthorized,
48 #[error("unauthorized: {0}")]
49 UnauthorizedMessage(String),
50 #[error("forbidden: {0}")]
51 Forbidden(String),
52 #[error("internal error: {0}")]
53 Internal(String),
54 #[error("validation error: {0}")]
55 Validation(String),
56 #[error("io error: {0}")]
57 Io(#[from] std::io::Error),
58 #[error("zip error: {0}")]
59 Zip(#[from] zip::result::ZipError),
60 #[error("docker error: {0}")]
61 Docker(String),
62 #[error("runner error: {0}")]
63 Runner(String),
64 #[error("runner capacity unavailable: {0}")]
65 RunnerCapacity(String),
66 #[error("base64 decode error")]
67 Base64,
68 #[error("{message}")]
69 ValidationDetails {
70 message: String,
71 details: Vec<ErrorDetail>,
72 },
73 #[error("payload too large: {0}")]
74 PayloadTooLarge(String),
75}
76
77impl ServiceErrorCode {
78 pub const fn as_str(self) -> &'static str {
80 match self {
81 ServiceErrorCode::BadRequest => "bad_request",
82 ServiceErrorCode::ValidationFailed => "validation_failed",
83 ServiceErrorCode::Unauthorized => "unauthorized",
84 ServiceErrorCode::Forbidden => "forbidden",
85 ServiceErrorCode::NotFound => "not_found",
86 ServiceErrorCode::Conflict => "conflict",
87 ServiceErrorCode::TooManyRequests => "too_many_requests",
88 ServiceErrorCode::PayloadTooLarge => "payload_too_large",
89 ServiceErrorCode::InternalError => "internal_error",
90 }
91 }
92}
93
94impl std::fmt::Display for ServiceErrorCode {
95 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 formatter.write_str(self.as_str())
97 }
98}
99
100impl ServiceError {
101 pub fn bad_request(message: impl Into<String>) -> Self {
103 Self::BadRequest(message.into())
104 }
105
106 pub fn validation_failed(
108 message: impl Into<String>,
109 details: impl Into<Vec<ErrorDetail>>,
110 ) -> Self {
111 Self::ValidationDetails {
112 message: message.into(),
113 details: details.into(),
114 }
115 }
116
117 pub fn not_found() -> Self {
119 Self::NotFound
120 }
121
122 pub fn conflict() -> Self {
124 Self::Conflict
125 }
126
127 pub fn conflict_with_details(
129 message: impl Into<String>,
130 details: impl Into<Vec<ErrorDetail>>,
131 ) -> Self {
132 Self::ConflictDetails {
133 message: message.into(),
134 details: details.into(),
135 }
136 }
137
138 pub fn too_many_requests(message: impl Into<String>) -> Self {
140 Self::TooManyRequests(message.into())
141 }
142
143 pub fn unauthorized(message: impl Into<String>) -> Self {
145 Self::UnauthorizedMessage(message.into())
146 }
147
148 pub fn internal(message: impl Into<String>) -> Self {
150 Self::Internal(message.into())
151 }
152
153 pub fn code(&self) -> ServiceErrorCode {
155 match self {
156 ServiceError::BadRequest(_) | ServiceError::Base64 | ServiceError::Zip(_) => {
157 ServiceErrorCode::BadRequest
158 }
159 ServiceError::Validation(_) | ServiceError::ValidationDetails { .. } => {
160 ServiceErrorCode::ValidationFailed
161 }
162 ServiceError::Unauthorized | ServiceError::UnauthorizedMessage(_) => {
163 ServiceErrorCode::Unauthorized
164 }
165 ServiceError::Forbidden(_) => ServiceErrorCode::Forbidden,
166 ServiceError::NotFound => ServiceErrorCode::NotFound,
167 ServiceError::Conflict | ServiceError::ConflictDetails { .. } => {
168 ServiceErrorCode::Conflict
169 }
170 ServiceError::TooManyRequests(_) => ServiceErrorCode::TooManyRequests,
171 ServiceError::PayloadTooLarge(_) => ServiceErrorCode::PayloadTooLarge,
172 ServiceError::Database(_)
173 | ServiceError::Internal(_)
174 | ServiceError::Io(_)
175 | ServiceError::Docker(_)
176 | ServiceError::Runner(_)
177 | ServiceError::RunnerCapacity(_) => ServiceErrorCode::InternalError,
178 }
179 }
180
181 pub fn public_message(&self) -> Cow<'_, str> {
183 match self {
184 ServiceError::BadRequest(message)
185 | ServiceError::TooManyRequests(message)
186 | ServiceError::Forbidden(message)
187 | ServiceError::Validation(message)
188 | ServiceError::PayloadTooLarge(message) => Cow::Borrowed(message),
189 ServiceError::ValidationDetails { message, .. } => Cow::Borrowed(message),
190 ServiceError::Unauthorized => Cow::Borrowed("unauthorized"),
191 ServiceError::UnauthorizedMessage(message) => Cow::Borrowed(message),
192 ServiceError::NotFound => Cow::Borrowed("not found"),
193 ServiceError::Conflict => Cow::Borrowed("conflict"),
194 ServiceError::ConflictDetails { message, .. } => Cow::Borrowed(message),
195 ServiceError::Base64 => Cow::Borrowed("invalid_base64"),
196 ServiceError::Zip(_) => Cow::Borrowed("invalid_zip"),
197 ServiceError::Database(_)
198 | ServiceError::Internal(_)
199 | ServiceError::Io(_)
200 | ServiceError::Docker(_)
201 | ServiceError::Runner(_)
202 | ServiceError::RunnerCapacity(_) => Cow::Borrowed("internal server error"),
203 }
204 }
205
206 pub fn details(&self) -> &[ErrorDetail] {
208 match self {
209 ServiceError::ValidationDetails { details, .. }
210 | ServiceError::ConflictDetails { details, .. } => details,
211 _ => &[],
212 }
213 }
214
215 pub fn is_internal(&self) -> bool {
217 matches!(self.code(), ServiceErrorCode::InternalError)
218 }
219
220 pub fn unique_violation_as_conflict(self) -> Self {
222 match self {
223 ServiceError::Database(sqlx::Error::Database(db_err))
224 if db_err.is_unique_violation() =>
225 {
226 ServiceError::Conflict
227 }
228 error => error,
229 }
230 }
231}
232
233pub type Result<T> = std::result::Result<T, ServiceError>;
234
235#[cfg(test)]
236mod tests {
237 use super::{ErrorDetail, ServiceError, ServiceErrorCode};
238
239 #[test]
240 fn constructors_preserve_public_error_data() {
241 let error = ServiceError::validation_failed(
242 "request validation failed",
243 [ErrorDetail {
244 field: Some("name".to_string()),
245 message: "required".to_string(),
246 }],
247 );
248
249 assert_eq!(error.code(), ServiceErrorCode::ValidationFailed);
250 assert_eq!(error.public_message(), "request validation failed");
251 assert_eq!(error.details().len(), 1);
252 }
253
254 #[test]
255 fn conflict_details_preserve_conflict_code_and_field_data() {
256 let error = ServiceError::conflict_with_details(
257 "duplicate token label",
258 [ErrorDetail {
259 field: Some("label".to_string()),
260 message: "already used".to_string(),
261 }],
262 );
263
264 assert_eq!(error.code(), ServiceErrorCode::Conflict);
265 assert_eq!(error.public_message(), "duplicate token label");
266 assert_eq!(
267 error.details(),
268 &[ErrorDetail {
269 field: Some("label".to_string()),
270 message: "already used".to_string(),
271 }]
272 );
273 }
274
275 #[test]
276 fn internal_errors_are_redacted() {
277 let error = ServiceError::internal("database password leaked here");
278
279 assert_eq!(error.code(), ServiceErrorCode::InternalError);
280 assert_eq!(error.public_message(), "internal server error");
281 assert!(error.details().is_empty());
282 }
283}