Skip to main content

oxillama_server/
error.rs

1//! Error types for the HTTP API server.
2
3use axum::http::StatusCode;
4use axum::response::{IntoResponse, Response};
5use thiserror::Error;
6
7/// Result type alias for server operations.
8pub type ServerResult<T> = Result<T, ServerError>;
9
10/// Errors that can occur in the API server.
11#[derive(Error, Debug)]
12pub enum ServerError {
13    /// Failed to bind to the specified address.
14    #[error("failed to bind to {addr}: {source}")]
15    BindError {
16        /// The address that failed to bind.
17        addr: String,
18        /// The underlying I/O error.
19        source: std::io::Error,
20    },
21
22    /// Error from the inference runtime.
23    #[error("runtime error: {0}")]
24    Runtime(#[from] oxillama_runtime::RuntimeError),
25
26    /// JSON serialization/deserialization error.
27    #[error("serialization error: {0}")]
28    Serialization(#[from] serde_json::Error),
29
30    /// Invalid request parameters.
31    #[error("invalid request: {message}")]
32    InvalidRequest {
33        /// Description of what was wrong.
34        message: String,
35    },
36
37    /// Model is not ready for inference.
38    #[error("model not ready")]
39    ModelNotReady,
40
41    /// The inference request queue is full; the server is overloaded.
42    #[error("inference queue is full — server overloaded")]
43    QueueFull,
44
45    /// The inference worker has exited; no new requests can be processed.
46    #[error("inference worker is no longer running")]
47    WorkerDead,
48
49    /// Thread not found in the persistent store.
50    #[error("thread not found: {0}")]
51    ThreadNotFound(String),
52
53    /// Run not found in the persistent store.
54    #[error("run not found: {0}")]
55    RunNotFound(String),
56
57    /// Attempted to transition a run that is already in a terminal state.
58    #[error("run is in terminal state: {0}")]
59    RunInTerminalState(String),
60
61    /// File not found in the persistent files store.
62    #[error("file not found: {0}")]
63    FileNotFound(String),
64
65    /// Uploaded file exceeds the maximum allowed size.
66    #[error("file too large: {0}")]
67    FileTooLarge(String),
68
69    /// Generic file store error.
70    #[error("file store error: {0}")]
71    FileStoreError(String),
72
73    /// Run step not found in the persistent store.
74    #[error("run step not found: {0}")]
75    RunStepNotFound(String),
76
77    /// Generic I/O error with context.
78    #[error("I/O error ({context}): {source}")]
79    IoError {
80        /// Human-readable context describing what operation failed.
81        context: String,
82        /// The underlying I/O error.
83        source: std::io::Error,
84    },
85
86    /// Response not found in the in-memory responses store.
87    #[error("response {0} not found")]
88    ResponseNotFound(String),
89
90    /// Previous response not found when chaining with `previous_response_id`.
91    #[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        // Construct a serde_json::Error via invalid JSON parsing.
187        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}