Skip to main content

bamboo_server/
error.rs

1//! Server error types and HTTP response handling
2//!
3//! This module provides a unified error handling system for the Actix-web server.
4//! All errors are converted to HTTP responses with appropriate status codes.
5//!
6//! # Error Types
7//!
8//! - `BadRequest`: Client errors (400)
9//! - `ToolNotFound`: Tool not available (404)
10//! - `ToolExecutionError`: Tool execution failed (400)
11//! - `ToolApprovalRequired`: Tool needs user approval (403)
12//! - `NotFound`: Resource not found (404)
13//! - `ProxyAuthRequired`: Proxy authentication needed (428)
14//! - `InternalError`: Server errors (500)
15//! - `StorageError`: File system errors (500)
16//! - `SerializationError`: JSON serialization errors (500)
17
18use actix_web::{http::StatusCode, HttpResponse, ResponseError};
19use serde::Serialize;
20use thiserror::Error;
21
22/// Result type alias for server operations
23pub type Result<T, E = AppError> = std::result::Result<T, E>;
24
25/// Application error enum with HTTP status code mapping
26#[derive(Debug, Error)]
27pub enum AppError {
28    #[error("Bad request: {0}")]
29    BadRequest(String),
30
31    #[error("Unauthorized: {0}")]
32    Unauthorized(String),
33
34    #[error("Forbidden: {0}")]
35    Forbidden(String),
36
37    #[error("Tool '{0}' not found")]
38    ToolNotFound(String),
39
40    #[error("Tool execution failed: {0}")]
41    ToolExecutionError(String),
42
43    #[error("Tool requires approval: {0}")]
44    ToolApprovalRequired(String),
45
46    #[error("{0} not found")]
47    NotFound(String),
48
49    #[error("Proxy authentication required")]
50    ProxyAuthRequired,
51
52    #[error("Internal server error: {0}")]
53    InternalError(#[from] anyhow::Error),
54
55    #[error("Storage error: {0}")]
56    StorageError(#[from] std::io::Error),
57
58    #[error("Serialization error: {0}")]
59    SerializationError(#[from] serde_json::Error),
60}
61
62#[derive(Serialize)]
63struct JsonError {
64    message: String,
65    r#type: String,
66    #[serde(skip_serializing_if = "Option::is_none")]
67    code: Option<String>,
68}
69
70#[derive(Serialize)]
71struct JsonErrorWrapper {
72    error: JsonError,
73}
74
75impl ResponseError for AppError {
76    fn status_code(&self) -> StatusCode {
77        match self {
78            AppError::BadRequest(_) => StatusCode::BAD_REQUEST,
79            AppError::Unauthorized(_) => StatusCode::UNAUTHORIZED,
80            AppError::Forbidden(_) => StatusCode::FORBIDDEN,
81            AppError::ToolNotFound(_) => StatusCode::NOT_FOUND,
82            AppError::ToolExecutionError(_) => StatusCode::BAD_REQUEST,
83            AppError::ToolApprovalRequired(_) => StatusCode::FORBIDDEN,
84            AppError::NotFound(_) => StatusCode::NOT_FOUND,
85            AppError::ProxyAuthRequired => StatusCode::PRECONDITION_REQUIRED,
86            AppError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR,
87            AppError::StorageError(_) => StatusCode::INTERNAL_SERVER_ERROR,
88            AppError::SerializationError(_) => StatusCode::INTERNAL_SERVER_ERROR,
89        }
90    }
91
92    fn error_response(&self) -> HttpResponse {
93        let status_code = self.status_code();
94        let error_response = JsonErrorWrapper {
95            error: JsonError {
96                message: self.to_string(),
97                r#type: "api_error".to_string(),
98                code: match self {
99                    AppError::ProxyAuthRequired => Some("proxy_auth_required".to_string()),
100                    _ => None,
101                },
102            },
103        };
104        HttpResponse::build(status_code).json(error_response)
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn test_app_error_bad_request() {
114        let err = AppError::BadRequest("Invalid input".to_string());
115        assert_eq!(err.to_string(), "Bad request: Invalid input");
116        assert_eq!(err.status_code(), StatusCode::BAD_REQUEST);
117    }
118
119    #[test]
120    fn test_app_error_tool_not_found() {
121        let err = AppError::ToolNotFound("bash".to_string());
122        assert_eq!(err.to_string(), "Tool 'bash' not found");
123        assert_eq!(err.status_code(), StatusCode::NOT_FOUND);
124    }
125
126    #[test]
127    fn test_app_error_tool_execution_error() {
128        let err = AppError::ToolExecutionError("Command failed".to_string());
129        assert_eq!(err.to_string(), "Tool execution failed: Command failed");
130        assert_eq!(err.status_code(), StatusCode::BAD_REQUEST);
131    }
132
133    #[test]
134    fn test_app_error_tool_approval_required() {
135        let err = AppError::ToolApprovalRequired("dangerous_tool".to_string());
136        assert_eq!(err.to_string(), "Tool requires approval: dangerous_tool");
137        assert_eq!(err.status_code(), StatusCode::FORBIDDEN);
138    }
139
140    #[test]
141    fn test_app_error_not_found() {
142        let err = AppError::NotFound("Session".to_string());
143        assert_eq!(err.to_string(), "Session not found");
144        assert_eq!(err.status_code(), StatusCode::NOT_FOUND);
145    }
146
147    #[test]
148    fn test_app_error_proxy_auth_required() {
149        let err = AppError::ProxyAuthRequired;
150        assert_eq!(err.to_string(), "Proxy authentication required");
151        assert_eq!(err.status_code(), StatusCode::PRECONDITION_REQUIRED);
152    }
153
154    #[test]
155    fn test_app_error_internal_error() {
156        let err = AppError::InternalError(anyhow::anyhow!("Something went wrong"));
157        assert!(err.to_string().contains("Something went wrong"));
158        assert_eq!(err.status_code(), StatusCode::INTERNAL_SERVER_ERROR);
159    }
160
161    #[test]
162    fn test_app_error_storage_error() {
163        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
164        let err = AppError::StorageError(io_err);
165        assert!(err.to_string().contains("file not found"));
166        assert_eq!(err.status_code(), StatusCode::INTERNAL_SERVER_ERROR);
167    }
168
169    #[test]
170    fn test_app_error_serialization_error() {
171        let json_err = serde_json::from_str::<i32>("invalid").unwrap_err();
172        let err = AppError::SerializationError(json_err);
173        assert!(err.to_string().contains("Serialization error"));
174        assert_eq!(err.status_code(), StatusCode::INTERNAL_SERVER_ERROR);
175    }
176
177    #[test]
178    fn test_error_response_bad_request() {
179        let err = AppError::BadRequest("Test error".to_string());
180        let response = err.error_response();
181        assert_eq!(response.status(), StatusCode::BAD_REQUEST);
182    }
183
184    #[test]
185    fn test_error_response_tool_not_found() {
186        let err = AppError::ToolNotFound("tool".to_string());
187        let response = err.error_response();
188        assert_eq!(response.status(), StatusCode::NOT_FOUND);
189    }
190
191    #[test]
192    fn test_error_response_proxy_auth_includes_code() {
193        let err = AppError::ProxyAuthRequired;
194        let response = err.error_response();
195        assert_eq!(response.status(), StatusCode::PRECONDITION_REQUIRED);
196    }
197
198    #[test]
199    fn test_app_error_debug() {
200        let err = AppError::BadRequest("test".to_string());
201        let debug_str = format!("{:?}", err);
202        assert!(debug_str.contains("BadRequest"));
203    }
204
205    #[test]
206    fn test_app_error_clone() {
207        let err1 = AppError::BadRequest("test".to_string());
208        // AppError derives Debug but not Clone
209        // This test verifies the Debug trait works
210        let debug_output = format!("{:?}", err1);
211        assert!(!debug_output.is_empty());
212    }
213
214    #[test]
215    fn test_result_type_ok() {
216        let result: Result<i32> = Ok(42);
217        assert!(result.is_ok());
218        assert_eq!(result.unwrap(), 42);
219    }
220
221    #[test]
222    fn test_result_type_err() {
223        let result: Result<i32> = Err(AppError::BadRequest("error".to_string()));
224        assert!(result.is_err());
225    }
226
227    #[test]
228    fn test_internal_error_from_anyhow() {
229        let anyhow_err = anyhow::anyhow!("Test error");
230        let app_error: AppError = anyhow_err.into();
231        assert!(matches!(app_error, AppError::InternalError(_)));
232    }
233
234    #[test]
235    fn test_storage_error_from_io() {
236        let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
237        let app_error: AppError = io_err.into();
238        assert!(matches!(app_error, AppError::StorageError(_)));
239    }
240
241    #[test]
242    fn test_serialization_error_from_serde_json() {
243        let json_err = serde_json::from_str::<bool>("not a bool").unwrap_err();
244        let app_error: AppError = json_err.into();
245        assert!(matches!(app_error, AppError::SerializationError(_)));
246    }
247}