1use axum::http::StatusCode;
4use axum::response::{IntoResponse, Response};
5use thiserror::Error;
6
7pub type ServerResult<T> = Result<T, ServerError>;
9
10#[derive(Error, Debug)]
12pub enum ServerError {
13 #[error("failed to bind to {addr}: {source}")]
15 BindError {
16 addr: String,
18 source: std::io::Error,
20 },
21
22 #[error("runtime error: {0}")]
24 Runtime(#[from] oxillama_runtime::RuntimeError),
25
26 #[error("serialization error: {0}")]
28 Serialization(#[from] serde_json::Error),
29
30 #[error("invalid request: {message}")]
32 InvalidRequest {
33 message: String,
35 },
36
37 #[error("model not ready")]
39 ModelNotReady,
40
41 #[error("inference queue is full — server overloaded")]
43 QueueFull,
44
45 #[error("inference worker is no longer running")]
47 WorkerDead,
48
49 #[error("thread not found: {0}")]
51 ThreadNotFound(String),
52
53 #[error("run not found: {0}")]
55 RunNotFound(String),
56
57 #[error("run is in terminal state: {0}")]
59 RunInTerminalState(String),
60
61 #[error("file not found: {0}")]
63 FileNotFound(String),
64
65 #[error("file too large: {0}")]
67 FileTooLarge(String),
68
69 #[error("file store error: {0}")]
71 FileStoreError(String),
72
73 #[error("run step not found: {0}")]
75 RunStepNotFound(String),
76
77 #[error("I/O error ({context}): {source}")]
79 IoError {
80 context: String,
82 source: std::io::Error,
84 },
85
86 #[error("response {0} not found")]
88 ResponseNotFound(String),
89
90 #[error("previous response {0} not found")]
92 PreviousResponseNotFound(String),
93}
94
95impl IntoResponse for ServerError {
96 fn into_response(self) -> Response {
97 let status = match &self {
98 ServerError::InvalidRequest { .. } => StatusCode::BAD_REQUEST,
99 ServerError::ModelNotReady => StatusCode::SERVICE_UNAVAILABLE,
100 ServerError::QueueFull => StatusCode::TOO_MANY_REQUESTS,
101 ServerError::WorkerDead => StatusCode::SERVICE_UNAVAILABLE,
102 ServerError::ThreadNotFound(_) => StatusCode::NOT_FOUND,
103 ServerError::RunNotFound(_) => StatusCode::NOT_FOUND,
104 ServerError::RunInTerminalState(_) => StatusCode::CONFLICT,
105 ServerError::FileNotFound(_) => StatusCode::NOT_FOUND,
106 ServerError::FileTooLarge(_) => StatusCode::PAYLOAD_TOO_LARGE,
107 ServerError::FileStoreError(_) => StatusCode::INTERNAL_SERVER_ERROR,
108 ServerError::RunStepNotFound(_) => StatusCode::NOT_FOUND,
109 ServerError::ResponseNotFound(_) => StatusCode::NOT_FOUND,
110 ServerError::PreviousResponseNotFound(_) => StatusCode::NOT_FOUND,
111 _ => StatusCode::INTERNAL_SERVER_ERROR,
112 };
113
114 let error_type = match &self {
115 ServerError::InvalidRequest { .. } => "invalid_request_error",
116 ServerError::ModelNotReady => "service_unavailable",
117 ServerError::QueueFull => "rate_limit_error",
118 ServerError::WorkerDead => "service_unavailable",
119 ServerError::ThreadNotFound(_) => "not_found_error",
120 ServerError::RunNotFound(_) => "not_found_error",
121 ServerError::RunInTerminalState(_) => "conflict_error",
122 ServerError::FileNotFound(_) => "not_found_error",
123 ServerError::FileTooLarge(_) => "payload_too_large",
124 ServerError::FileStoreError(_) => "internal_error",
125 ServerError::RunStepNotFound(_) => "not_found_error",
126 ServerError::ResponseNotFound(_) => "not_found_error",
127 ServerError::PreviousResponseNotFound(_) => "not_found_error",
128 _ => "internal_error",
129 };
130
131 let body = serde_json::json!({
132 "error": {
133 "message": self.to_string(),
134 "type": error_type,
135 }
136 });
137
138 (status, axum::Json(body)).into_response()
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use axum::response::IntoResponse;
146
147 fn status_of(err: ServerError) -> StatusCode {
148 let resp = err.into_response();
149 resp.status()
150 }
151
152 #[test]
153 fn test_invalid_request_returns_400() {
154 let err = ServerError::InvalidRequest {
155 message: "bad param".to_string(),
156 };
157 assert_eq!(status_of(err), StatusCode::BAD_REQUEST);
158 }
159
160 #[test]
161 fn test_model_not_ready_returns_503() {
162 assert_eq!(
163 status_of(ServerError::ModelNotReady),
164 StatusCode::SERVICE_UNAVAILABLE
165 );
166 }
167
168 #[test]
169 fn test_queue_full_returns_429() {
170 assert_eq!(
171 status_of(ServerError::QueueFull),
172 StatusCode::TOO_MANY_REQUESTS
173 );
174 }
175
176 #[test]
177 fn test_worker_dead_returns_503() {
178 assert_eq!(
179 status_of(ServerError::WorkerDead),
180 StatusCode::SERVICE_UNAVAILABLE
181 );
182 }
183
184 #[test]
185 fn test_serialization_error_returns_500() {
186 let json_err = serde_json::from_str::<serde_json::Value>("not json")
188 .expect_err("parsing invalid JSON should fail");
189 let err = ServerError::Serialization(json_err);
190 assert_eq!(status_of(err), StatusCode::INTERNAL_SERVER_ERROR);
191 }
192
193 #[test]
194 fn test_error_display_invalid_request() {
195 let err = ServerError::InvalidRequest {
196 message: "missing field".to_string(),
197 };
198 let msg = err.to_string();
199 assert!(
200 msg.contains("missing field"),
201 "display should contain message: {msg}"
202 );
203 }
204
205 #[test]
206 fn test_error_display_model_not_ready() {
207 let msg = ServerError::ModelNotReady.to_string();
208 assert!(!msg.is_empty());
209 }
210
211 #[test]
212 fn test_error_display_queue_full() {
213 let msg = ServerError::QueueFull.to_string();
214 assert!(!msg.is_empty());
215 }
216
217 #[test]
218 fn test_error_display_worker_dead() {
219 let msg = ServerError::WorkerDead.to_string();
220 assert!(!msg.is_empty());
221 }
222
223 #[test]
224 fn test_thread_not_found_returns_404() {
225 assert_eq!(
226 status_of(ServerError::ThreadNotFound("thread_xyz".into())),
227 StatusCode::NOT_FOUND
228 );
229 }
230
231 #[test]
232 fn test_run_not_found_returns_404() {
233 assert_eq!(
234 status_of(ServerError::RunNotFound("run_xyz".into())),
235 StatusCode::NOT_FOUND
236 );
237 }
238
239 #[test]
240 fn test_run_in_terminal_state_returns_409() {
241 assert_eq!(
242 status_of(ServerError::RunInTerminalState(
243 "run_xyz is completed".into()
244 )),
245 StatusCode::CONFLICT
246 );
247 }
248}