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