Skip to main content

systemprompt_models/api/
errors.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4
5#[cfg(feature = "web")]
6use axum::Json;
7#[cfg(feature = "web")]
8use axum::http::{StatusCode, header};
9#[cfg(feature = "web")]
10use axum::response::IntoResponse;
11
12#[derive(Debug, thiserror::Error)]
13pub enum InternalApiError {
14    #[error("Resource not found: {resource_type} with ID '{id}'")]
15    NotFound { resource_type: String, id: String },
16
17    #[error("Bad request: {message}")]
18    BadRequest { message: String },
19
20    #[error("Unauthorized access: {reason}")]
21    Unauthorized { reason: String },
22
23    #[error("Access forbidden: {resource} - {reason}")]
24    Forbidden { resource: String, reason: String },
25
26    #[error("Validation failed for field '{field}': {reason}")]
27    ValidationError { field: String, reason: String },
28
29    #[error("Conflict: {resource} already exists")]
30    ConflictError { resource: String },
31
32    #[error("Rate limit exceeded for {resource}")]
33    RateLimited { resource: String },
34
35    #[error("Service temporarily unavailable: {service}")]
36    ServiceUnavailable { service: String },
37
38    #[error("Database operation failed: {message}")]
39    DatabaseError { message: String },
40
41    #[error("JSON serialization failed")]
42    JsonError(#[from] serde_json::Error),
43
44    #[error("Authentication token error: {message}")]
45    AuthenticationError { message: String },
46
47    #[error("Internal server error: {message}")]
48    InternalError { message: String },
49}
50
51impl InternalApiError {
52    pub fn not_found(resource_type: impl Into<String>, id: impl Into<String>) -> Self {
53        Self::NotFound {
54            resource_type: resource_type.into(),
55            id: id.into(),
56        }
57    }
58
59    pub fn bad_request(message: impl Into<String>) -> Self {
60        Self::BadRequest {
61            message: message.into(),
62        }
63    }
64
65    pub fn unauthorized(reason: impl Into<String>) -> Self {
66        Self::Unauthorized {
67            reason: reason.into(),
68        }
69    }
70
71    pub fn forbidden(resource: impl Into<String>, reason: impl Into<String>) -> Self {
72        Self::Forbidden {
73            resource: resource.into(),
74            reason: reason.into(),
75        }
76    }
77
78    pub fn validation_error(field: impl Into<String>, reason: impl Into<String>) -> Self {
79        Self::ValidationError {
80            field: field.into(),
81            reason: reason.into(),
82        }
83    }
84
85    pub fn conflict(resource: impl Into<String>) -> Self {
86        Self::ConflictError {
87            resource: resource.into(),
88        }
89    }
90
91    pub fn rate_limited(resource: impl Into<String>) -> Self {
92        Self::RateLimited {
93            resource: resource.into(),
94        }
95    }
96
97    pub fn service_unavailable(service: impl Into<String>) -> Self {
98        Self::ServiceUnavailable {
99            service: service.into(),
100        }
101    }
102
103    pub fn internal_error(message: impl Into<String>) -> Self {
104        Self::InternalError {
105            message: message.into(),
106        }
107    }
108
109    pub fn database_error(message: impl Into<String>) -> Self {
110        Self::DatabaseError {
111            message: message.into(),
112        }
113    }
114
115    pub fn authentication_error(message: impl Into<String>) -> Self {
116        Self::AuthenticationError {
117            message: message.into(),
118        }
119    }
120
121    pub const fn error_code(&self) -> ErrorCode {
122        match self {
123            Self::NotFound { .. } => ErrorCode::NotFound,
124            Self::BadRequest { .. } => ErrorCode::BadRequest,
125            Self::Unauthorized { .. } => ErrorCode::Unauthorized,
126            Self::Forbidden { .. } => ErrorCode::Forbidden,
127            Self::ValidationError { .. } => ErrorCode::ValidationError,
128            Self::ConflictError { .. } => ErrorCode::ConflictError,
129            Self::RateLimited { .. } => ErrorCode::RateLimited,
130            Self::ServiceUnavailable { .. } => ErrorCode::ServiceUnavailable,
131            Self::DatabaseError { .. }
132            | Self::JsonError(_)
133            | Self::AuthenticationError { .. }
134            | Self::InternalError { .. } => ErrorCode::InternalError,
135        }
136    }
137}
138
139impl From<InternalApiError> for ApiError {
140    fn from(error: InternalApiError) -> Self {
141        let code = error.error_code();
142        let message = error.to_string();
143        let details = match &error {
144            InternalApiError::NotFound { resource_type, id } => Some(format!(
145                "The requested {resource_type} with ID '{id}' does not exist"
146            )),
147            InternalApiError::ValidationError { field, reason } => {
148                Some(format!("Field '{field}': {reason}"))
149            },
150            InternalApiError::Forbidden { resource, reason } => {
151                Some(format!("Access to {resource} denied: {reason}"))
152            },
153            InternalApiError::DatabaseError { message } => {
154                Some(format!("Database error: {message}"))
155            },
156            InternalApiError::JsonError(e) => Some(format!("JSON processing error: {e}")),
157            InternalApiError::AuthenticationError { message } => {
158                Some(format!("Authentication error: {message}"))
159            },
160            InternalApiError::BadRequest { .. }
161            | InternalApiError::Unauthorized { .. }
162            | InternalApiError::ConflictError { .. }
163            | InternalApiError::RateLimited { .. }
164            | InternalApiError::ServiceUnavailable { .. }
165            | InternalApiError::InternalError { .. } => None,
166        };
167
168        let api_error = Self::new(code, message);
169        if let Some(d) = details {
170            api_error.with_details(d)
171        } else {
172            api_error
173        }
174    }
175}
176
177#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
178#[serde(rename_all = "snake_case")]
179pub enum ErrorCode {
180    NotFound,
181    BadRequest,
182    Unauthorized,
183    Forbidden,
184    InternalError,
185    ValidationError,
186    ConflictError,
187    RateLimited,
188    ServiceUnavailable,
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct ValidationError {
193    pub field: String,
194
195    pub message: String,
196
197    pub code: String,
198
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub context: Option<Value>,
201}
202
203#[derive(Debug, Serialize, Deserialize)]
204pub struct ApiError {
205    pub code: ErrorCode,
206
207    pub message: String,
208
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub details: Option<String>,
211
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub error_key: Option<String>,
214
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub path: Option<String>,
217
218    #[serde(default, skip_serializing_if = "Vec::is_empty")]
219    pub validation_errors: Vec<ValidationError>,
220
221    pub timestamp: DateTime<Utc>,
222
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub trace_id: Option<String>,
225}
226
227impl ApiError {
228    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
229        Self {
230            code,
231            message: message.into(),
232            details: None,
233            error_key: None,
234            path: None,
235            validation_errors: Vec::new(),
236            timestamp: Utc::now(),
237            trace_id: None,
238        }
239    }
240
241    pub fn with_details(mut self, details: impl Into<String>) -> Self {
242        self.details = Some(details.into());
243        self
244    }
245
246    pub fn with_error_key(mut self, key: impl Into<String>) -> Self {
247        self.error_key = Some(key.into());
248        self
249    }
250
251    pub fn with_path(mut self, path: impl Into<String>) -> Self {
252        self.path = Some(path.into());
253        self
254    }
255
256    pub fn with_validation_errors(mut self, errors: Vec<ValidationError>) -> Self {
257        self.validation_errors = errors;
258        self
259    }
260
261    pub fn with_trace_id(mut self, id: impl Into<String>) -> Self {
262        self.trace_id = Some(id.into());
263        self
264    }
265
266    pub fn not_found(message: impl Into<String>) -> Self {
267        Self::new(ErrorCode::NotFound, message)
268    }
269
270    pub fn bad_request(message: impl Into<String>) -> Self {
271        Self::new(ErrorCode::BadRequest, message)
272    }
273
274    pub fn unauthorized(message: impl Into<String>) -> Self {
275        Self::new(ErrorCode::Unauthorized, message)
276    }
277
278    pub fn forbidden(message: impl Into<String>) -> Self {
279        Self::new(ErrorCode::Forbidden, message)
280    }
281
282    pub fn internal_error(message: impl Into<String>) -> Self {
283        Self::new(ErrorCode::InternalError, message)
284    }
285
286    pub fn validation_error(message: impl Into<String>, errors: Vec<ValidationError>) -> Self {
287        Self::new(ErrorCode::ValidationError, message).with_validation_errors(errors)
288    }
289
290    pub fn conflict(message: impl Into<String>) -> Self {
291        Self::new(ErrorCode::ConflictError, message)
292    }
293}
294
295#[derive(Debug, Serialize, Deserialize)]
296pub struct ErrorResponse {
297    pub error: ApiError,
298
299    pub api_version: String,
300}
301
302#[cfg(feature = "web")]
303impl ErrorCode {
304    pub const fn status_code(&self) -> StatusCode {
305        match self {
306            Self::NotFound => StatusCode::NOT_FOUND,
307            Self::BadRequest => StatusCode::BAD_REQUEST,
308            Self::Unauthorized => StatusCode::UNAUTHORIZED,
309            Self::Forbidden => StatusCode::FORBIDDEN,
310            Self::ValidationError => StatusCode::UNPROCESSABLE_ENTITY,
311            Self::ConflictError => StatusCode::CONFLICT,
312            Self::RateLimited => StatusCode::TOO_MANY_REQUESTS,
313            Self::ServiceUnavailable => StatusCode::SERVICE_UNAVAILABLE,
314            Self::InternalError => StatusCode::INTERNAL_SERVER_ERROR,
315        }
316    }
317}
318
319#[cfg(feature = "web")]
320impl IntoResponse for ApiError {
321    fn into_response(self) -> axum::response::Response {
322        let status = self.code.status_code();
323
324        if status.is_server_error() {
325            tracing::error!(
326                error_code = ?self.code,
327                message = %self.message,
328                path = ?self.path,
329                trace_id = ?self.trace_id,
330                "API server error response"
331            );
332        } else if status.is_client_error() {
333            tracing::warn!(
334                error_code = ?self.code,
335                message = %self.message,
336                path = ?self.path,
337                trace_id = ?self.trace_id,
338                "API client error response"
339            );
340        }
341
342        let mut response = (status, Json(self)).into_response();
343
344        if status == StatusCode::UNAUTHORIZED {
345            if let Ok(header_value) =
346                "Bearer resource_metadata=\"/.well-known/oauth-protected-resource\"".parse()
347            {
348                response
349                    .headers_mut()
350                    .insert(header::WWW_AUTHENTICATE, header_value);
351            }
352        }
353
354        response
355    }
356}
357
358#[cfg(feature = "web")]
359impl IntoResponse for InternalApiError {
360    fn into_response(self) -> axum::response::Response {
361        let error: ApiError = self.into();
362        error.into_response()
363    }
364}