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