1use actix_web::{http::StatusCode, HttpResponse, ResponseError};
19use serde::Serialize;
20use thiserror::Error;
21
22pub type Result<T, E = AppError> = std::result::Result<T, E>;
24
25#[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 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}