systemprompt_models/api/errors/
mod.rs1mod internal;
10
11pub use internal::InternalApiError;
12
13use chrono::{DateTime, Utc};
14use serde::{Deserialize, Serialize};
15use serde_json::Value;
16
17#[cfg(feature = "web")]
18use axum::Json;
19#[cfg(feature = "web")]
20use axum::http::{StatusCode, header};
21#[cfg(feature = "web")]
22use axum::response::IntoResponse;
23
24#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
25#[serde(rename_all = "snake_case")]
26pub enum ErrorCode {
27 NotFound,
28 BadRequest,
29 Unauthorized,
30 Forbidden,
31 InternalError,
32 ValidationError,
33 ConflictError,
34 RateLimited,
35 ServiceUnavailable,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct ValidationError {
40 pub field: String,
41 pub message: String,
42 pub code: String,
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub context: Option<Value>,
46}
47
48#[derive(Debug, Serialize, Deserialize)]
49pub struct ApiError {
50 pub code: ErrorCode,
51 pub message: String,
52 #[serde(skip_serializing_if = "Option::is_none")]
53 pub details: Option<String>,
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub error_key: Option<String>,
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub path: Option<String>,
58 #[serde(default, skip_serializing_if = "Vec::is_empty")]
59 pub validation_errors: Vec<ValidationError>,
60 pub timestamp: DateTime<Utc>,
61 #[serde(skip_serializing_if = "Option::is_none")]
62 pub trace_id: Option<String>,
63}
64
65impl ApiError {
66 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
67 Self {
68 code,
69 message: message.into(),
70 details: None,
71 error_key: None,
72 path: None,
73 validation_errors: Vec::new(),
74 timestamp: Utc::now(),
75 trace_id: None,
76 }
77 }
78
79 #[must_use]
80 pub fn with_details(mut self, details: impl Into<String>) -> Self {
81 self.details = Some(details.into());
82 self
83 }
84
85 #[must_use]
86 pub fn with_error_key(mut self, key: impl Into<String>) -> Self {
87 self.error_key = Some(key.into());
88 self
89 }
90
91 #[must_use]
92 pub fn with_path(mut self, path: impl Into<String>) -> Self {
93 self.path = Some(path.into());
94 self
95 }
96
97 #[must_use]
98 pub fn with_validation_errors(mut self, errors: Vec<ValidationError>) -> Self {
99 self.validation_errors = errors;
100 self
101 }
102
103 #[must_use]
104 pub fn with_trace_id(mut self, id: impl Into<String>) -> Self {
105 self.trace_id = Some(id.into());
106 self
107 }
108
109 pub fn not_found(message: impl Into<String>) -> Self {
110 Self::new(ErrorCode::NotFound, message)
111 }
112
113 pub fn bad_request(message: impl Into<String>) -> Self {
114 Self::new(ErrorCode::BadRequest, message)
115 }
116
117 pub fn unauthorized(message: impl Into<String>) -> Self {
118 Self::new(ErrorCode::Unauthorized, message)
119 }
120
121 pub fn forbidden(message: impl Into<String>) -> Self {
122 Self::new(ErrorCode::Forbidden, message)
123 }
124
125 pub fn internal_error(message: impl Into<String>) -> Self {
126 Self::new(ErrorCode::InternalError, message)
127 }
128
129 pub fn validation_error(message: impl Into<String>, errors: Vec<ValidationError>) -> Self {
130 Self::new(ErrorCode::ValidationError, message).with_validation_errors(errors)
131 }
132
133 pub fn conflict(message: impl Into<String>) -> Self {
134 Self::new(ErrorCode::ConflictError, message)
135 }
136}
137
138#[derive(Debug, Serialize, Deserialize)]
139pub struct ErrorResponse {
140 pub error: ApiError,
141 pub api_version: String,
142}
143
144#[cfg(feature = "web")]
145impl ErrorCode {
146 #[must_use]
147 pub const fn status_code(&self) -> StatusCode {
148 match self {
149 Self::NotFound => StatusCode::NOT_FOUND,
150 Self::BadRequest => StatusCode::BAD_REQUEST,
151 Self::Unauthorized => StatusCode::UNAUTHORIZED,
152 Self::Forbidden => StatusCode::FORBIDDEN,
153 Self::ValidationError => StatusCode::UNPROCESSABLE_ENTITY,
154 Self::ConflictError => StatusCode::CONFLICT,
155 Self::RateLimited => StatusCode::TOO_MANY_REQUESTS,
156 Self::ServiceUnavailable => StatusCode::SERVICE_UNAVAILABLE,
157 Self::InternalError => StatusCode::INTERNAL_SERVER_ERROR,
158 }
159 }
160}
161
162#[cfg(feature = "web")]
163impl IntoResponse for ApiError {
164 fn into_response(self) -> axum::response::Response {
165 let status = self.code.status_code();
166
167 if status.is_server_error() {
168 tracing::error!(
169 error_code = ?self.code,
170 message = %self.message,
171 path = ?self.path,
172 trace_id = ?self.trace_id,
173 "API server error response"
174 );
175 } else if status.is_client_error() {
176 tracing::warn!(
177 error_code = ?self.code,
178 message = %self.message,
179 path = ?self.path,
180 trace_id = ?self.trace_id,
181 "API client error response"
182 );
183 }
184
185 let mut response = (status, Json(self)).into_response();
186
187 if status == StatusCode::UNAUTHORIZED
188 && let Ok(header_value) =
189 "Bearer resource_metadata=\"/.well-known/oauth-protected-resource\"".parse()
190 {
191 response
192 .headers_mut()
193 .insert(header::WWW_AUTHENTICATE, header_value);
194 }
195
196 response
197 }
198}