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("Config recovery pending confirmation: {0}")]
56 ConfigRecoveryPending(String),
57
58 #[error("Internal server error: {0}")]
59 InternalError(#[from] anyhow::Error),
60
61 #[error("Storage error: {0}")]
62 StorageError(#[from] std::io::Error),
63
64 #[error("Serialization error: {0}")]
65 SerializationError(#[from] serde_json::Error),
66}
67
68#[derive(Serialize)]
69struct JsonError {
70 message: String,
71 r#type: String,
72 #[serde(skip_serializing_if = "Option::is_none")]
73 code: Option<String>,
74}
75
76#[derive(Serialize)]
77struct JsonErrorWrapper {
78 error: JsonError,
79}
80
81pub fn error_value(message: impl Into<String>) -> serde_json::Value {
102 serde_json::json!({ "message": message.into(), "type": "api_error" })
103}
104
105impl ResponseError for AppError {
106 fn status_code(&self) -> StatusCode {
107 match self {
108 AppError::BadRequest(_) => StatusCode::BAD_REQUEST,
109 AppError::Unauthorized(_) => StatusCode::UNAUTHORIZED,
110 AppError::Forbidden(_) => StatusCode::FORBIDDEN,
111 AppError::ToolNotFound(_) => StatusCode::NOT_FOUND,
112 AppError::ToolExecutionError(_) => StatusCode::BAD_REQUEST,
113 AppError::ToolApprovalRequired(_) => StatusCode::FORBIDDEN,
114 AppError::NotFound(_) => StatusCode::NOT_FOUND,
115 AppError::ProxyAuthRequired => StatusCode::PRECONDITION_REQUIRED,
116 AppError::ConfigRecoveryPending(_) => StatusCode::CONFLICT,
117 AppError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR,
118 AppError::StorageError(_) => StatusCode::INTERNAL_SERVER_ERROR,
119 AppError::SerializationError(_) => StatusCode::INTERNAL_SERVER_ERROR,
120 }
121 }
122
123 fn error_response(&self) -> HttpResponse {
124 let status_code = self.status_code();
125 let error_response = JsonErrorWrapper {
126 error: JsonError {
127 message: self.to_string(),
128 r#type: "api_error".to_string(),
129 code: match self {
130 AppError::ProxyAuthRequired => Some("proxy_auth_required".to_string()),
131 AppError::ConfigRecoveryPending(_) => {
132 Some("config_recovery_pending".to_string())
133 }
134 _ => None,
135 },
136 },
137 };
138 HttpResponse::build(status_code).json(error_response)
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn test_app_error_bad_request() {
148 let err = AppError::BadRequest("Invalid input".to_string());
149 assert_eq!(err.to_string(), "Bad request: Invalid input");
150 assert_eq!(err.status_code(), StatusCode::BAD_REQUEST);
151 }
152
153 #[test]
154 fn test_app_error_tool_not_found() {
155 let err = AppError::ToolNotFound("bash".to_string());
156 assert_eq!(err.to_string(), "Tool 'bash' not found");
157 assert_eq!(err.status_code(), StatusCode::NOT_FOUND);
158 }
159
160 #[test]
161 fn test_app_error_tool_execution_error() {
162 let err = AppError::ToolExecutionError("Command failed".to_string());
163 assert_eq!(err.to_string(), "Tool execution failed: Command failed");
164 assert_eq!(err.status_code(), StatusCode::BAD_REQUEST);
165 }
166
167 #[test]
168 fn test_app_error_tool_approval_required() {
169 let err = AppError::ToolApprovalRequired("dangerous_tool".to_string());
170 assert_eq!(err.to_string(), "Tool requires approval: dangerous_tool");
171 assert_eq!(err.status_code(), StatusCode::FORBIDDEN);
172 }
173
174 #[test]
175 fn test_app_error_not_found() {
176 let err = AppError::NotFound("Session".to_string());
177 assert_eq!(err.to_string(), "Session not found");
178 assert_eq!(err.status_code(), StatusCode::NOT_FOUND);
179 }
180
181 #[test]
182 fn test_app_error_proxy_auth_required() {
183 let err = AppError::ProxyAuthRequired;
184 assert_eq!(err.to_string(), "Proxy authentication required");
185 assert_eq!(err.status_code(), StatusCode::PRECONDITION_REQUIRED);
186 }
187
188 #[test]
189 fn test_app_error_internal_error() {
190 let err = AppError::InternalError(anyhow::anyhow!("Something went wrong"));
191 assert!(err.to_string().contains("Something went wrong"));
192 assert_eq!(err.status_code(), StatusCode::INTERNAL_SERVER_ERROR);
193 }
194
195 #[test]
196 fn test_app_error_storage_error() {
197 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
198 let err = AppError::StorageError(io_err);
199 assert!(err.to_string().contains("file not found"));
200 assert_eq!(err.status_code(), StatusCode::INTERNAL_SERVER_ERROR);
201 }
202
203 #[test]
204 fn test_app_error_serialization_error() {
205 let json_err = serde_json::from_str::<i32>("invalid").unwrap_err();
206 let err = AppError::SerializationError(json_err);
207 assert!(err.to_string().contains("Serialization error"));
208 assert_eq!(err.status_code(), StatusCode::INTERNAL_SERVER_ERROR);
209 }
210
211 #[test]
212 fn test_error_response_bad_request() {
213 let err = AppError::BadRequest("Test error".to_string());
214 let response = err.error_response();
215 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
216 }
217
218 #[test]
219 fn test_error_response_tool_not_found() {
220 let err = AppError::ToolNotFound("tool".to_string());
221 let response = err.error_response();
222 assert_eq!(response.status(), StatusCode::NOT_FOUND);
223 }
224
225 #[test]
226 fn test_error_response_proxy_auth_includes_code() {
227 let err = AppError::ProxyAuthRequired;
228 let response = err.error_response();
229 assert_eq!(response.status(), StatusCode::PRECONDITION_REQUIRED);
230 }
231
232 #[test]
233 fn test_app_error_debug() {
234 let err = AppError::BadRequest("test".to_string());
235 let debug_str = format!("{:?}", err);
236 assert!(debug_str.contains("BadRequest"));
237 }
238
239 #[test]
240 fn test_app_error_clone() {
241 let err1 = AppError::BadRequest("test".to_string());
242 let debug_output = format!("{:?}", err1);
245 assert!(!debug_output.is_empty());
246 }
247
248 #[test]
249 fn test_result_type_ok() {
250 let result: Result<i32> = Ok(42);
251 assert!(result.is_ok());
252 assert!(matches!(result, Ok(42)));
253 }
254
255 #[test]
256 fn test_result_type_err() {
257 let result: Result<i32> = Err(AppError::BadRequest("error".to_string()));
258 assert!(result.is_err());
259 }
260
261 #[test]
262 fn test_internal_error_from_anyhow() {
263 let anyhow_err = anyhow::anyhow!("Test error");
264 let app_error: AppError = anyhow_err.into();
265 assert!(matches!(app_error, AppError::InternalError(_)));
266 }
267
268 #[test]
269 fn test_storage_error_from_io() {
270 let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
271 let app_error: AppError = io_err.into();
272 assert!(matches!(app_error, AppError::StorageError(_)));
273 }
274
275 #[test]
276 fn test_serialization_error_from_serde_json() {
277 let json_err = serde_json::from_str::<bool>("not a bool").unwrap_err();
278 let app_error: AppError = json_err.into();
279 assert!(matches!(app_error, AppError::SerializationError(_)));
280 }
281
282 #[actix_web::test]
287 async fn app_error_and_error_value_agree_on_envelope_shape() {
288 let resp = AppError::NotFound("Session".to_string()).error_response();
289 let bytes = actix_web::body::to_bytes(resp.into_body()).await.unwrap();
290 let app_err_body: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
291 assert_eq!(app_err_body["error"]["type"], "api_error");
292 assert!(app_err_body["error"]["message"].is_string());
293
294 let helper_error = error_value("Session not found");
295 assert_eq!(helper_error["type"], "api_error");
296 assert_eq!(helper_error["message"], "Session not found");
297
298 assert_eq!(
300 app_err_body["error"]["type"], helper_error["type"],
301 "AppError and error_value must use the same error \"type\" tag"
302 );
303 }
304}