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::http::{header, StatusCode};
7#[cfg(feature = "web")]
8use axum::response::IntoResponse;
9#[cfg(feature = "web")]
10use axum::Json;
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    pub context: Option<Value>,
200}
201
202#[derive(Debug, Serialize, Deserialize)]
203pub struct ApiError {
204    pub code: ErrorCode,
205
206    pub message: String,
207
208    pub details: Option<String>,
209
210    pub error_key: Option<String>,
211
212    pub path: Option<String>,
213
214    #[serde(default)]
215    pub validation_errors: Vec<ValidationError>,
216
217    pub timestamp: DateTime<Utc>,
218
219    pub trace_id: Option<String>,
220}
221
222impl ApiError {
223    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
224        Self {
225            code,
226            message: message.into(),
227            details: None,
228            error_key: None,
229            path: None,
230            validation_errors: Vec::new(),
231            timestamp: Utc::now(),
232            trace_id: None,
233        }
234    }
235
236    pub fn with_details(mut self, details: impl Into<String>) -> Self {
237        self.details = Some(details.into());
238        self
239    }
240
241    pub fn with_error_key(mut self, key: impl Into<String>) -> Self {
242        self.error_key = Some(key.into());
243        self
244    }
245
246    pub fn with_path(mut self, path: impl Into<String>) -> Self {
247        self.path = Some(path.into());
248        self
249    }
250
251    pub fn with_validation_errors(mut self, errors: Vec<ValidationError>) -> Self {
252        self.validation_errors = errors;
253        self
254    }
255
256    pub fn with_trace_id(mut self, id: impl Into<String>) -> Self {
257        self.trace_id = Some(id.into());
258        self
259    }
260
261    pub fn not_found(message: impl Into<String>) -> Self {
262        Self::new(ErrorCode::NotFound, message)
263    }
264
265    pub fn bad_request(message: impl Into<String>) -> Self {
266        Self::new(ErrorCode::BadRequest, message)
267    }
268
269    pub fn unauthorized(message: impl Into<String>) -> Self {
270        Self::new(ErrorCode::Unauthorized, message)
271    }
272
273    pub fn forbidden(message: impl Into<String>) -> Self {
274        Self::new(ErrorCode::Forbidden, message)
275    }
276
277    pub fn internal_error(message: impl Into<String>) -> Self {
278        Self::new(ErrorCode::InternalError, message)
279    }
280
281    pub fn validation_error(message: impl Into<String>, errors: Vec<ValidationError>) -> Self {
282        Self::new(ErrorCode::ValidationError, message).with_validation_errors(errors)
283    }
284
285    pub fn conflict(message: impl Into<String>) -> Self {
286        Self::new(ErrorCode::ConflictError, message)
287    }
288}
289
290#[derive(Debug, Serialize, Deserialize)]
291pub struct ErrorResponse {
292    pub error: ApiError,
293
294    pub api_version: String,
295}
296
297#[cfg(feature = "web")]
298impl ErrorCode {
299    pub const fn status_code(&self) -> StatusCode {
300        match self {
301            Self::NotFound => StatusCode::NOT_FOUND,
302            Self::BadRequest => StatusCode::BAD_REQUEST,
303            Self::Unauthorized => StatusCode::UNAUTHORIZED,
304            Self::Forbidden => StatusCode::FORBIDDEN,
305            Self::ValidationError => StatusCode::UNPROCESSABLE_ENTITY,
306            Self::ConflictError => StatusCode::CONFLICT,
307            Self::RateLimited => StatusCode::TOO_MANY_REQUESTS,
308            Self::ServiceUnavailable => StatusCode::SERVICE_UNAVAILABLE,
309            Self::InternalError => StatusCode::INTERNAL_SERVER_ERROR,
310        }
311    }
312}
313
314#[cfg(feature = "web")]
315impl IntoResponse for ApiError {
316    fn into_response(self) -> axum::response::Response {
317        let status = self.code.status_code();
318
319        if status.is_server_error() {
320            tracing::error!(
321                error_code = ?self.code,
322                message = %self.message,
323                path = ?self.path,
324                trace_id = ?self.trace_id,
325                "API server error response"
326            );
327        } else if status.is_client_error() {
328            tracing::warn!(
329                error_code = ?self.code,
330                message = %self.message,
331                path = ?self.path,
332                trace_id = ?self.trace_id,
333                "API client error response"
334            );
335        }
336
337        let mut response = (status, Json(self)).into_response();
338
339        if status == StatusCode::UNAUTHORIZED {
340            if let Ok(header_value) =
341                "Bearer resource_metadata=\"/.well-known/oauth-protected-resource\"".parse()
342            {
343                response
344                    .headers_mut()
345                    .insert(header::WWW_AUTHENTICATE, header_value);
346            }
347        }
348
349        response
350    }
351}
352
353#[cfg(feature = "web")]
354impl IntoResponse for InternalApiError {
355    fn into_response(self) -> axum::response::Response {
356        let error: ApiError = self.into();
357        error.into_response()
358    }
359}