turbomcp_core/
error.rs

1//! Comprehensive error handling with rich context preservation.
2//!
3//! This module provides a sophisticated error handling system that captures
4//! detailed context about failures, supports error chaining, and integrates
5//! with observability systems.
6
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::fmt;
10use uuid::Uuid;
11
12#[cfg(feature = "fancy-errors")]
13use miette::Diagnostic;
14
15/// Result type alias for MCP operations
16pub type Result<T> = std::result::Result<T, Box<Error>>;
17
18/// Comprehensive error type with rich context information
19#[derive(Debug, Serialize)]
20#[cfg_attr(feature = "fancy-errors", derive(Diagnostic))]
21pub struct Error {
22    /// Unique identifier for this error instance
23    pub id: Uuid,
24
25    /// Error classification
26    pub kind: ErrorKind,
27
28    /// Human-readable error message
29    pub message: String,
30
31    /// Additional contextual information
32    pub context: ErrorContext,
33
34    /// Optional source error that caused this error
35    #[serde(skip)]
36    pub source: Option<Box<Error>>,
37
38    /// Stack trace information (when available)
39    #[cfg(debug_assertions)]
40    #[serde(skip)]
41    pub backtrace: std::backtrace::Backtrace,
42}
43
44impl Clone for Error {
45    fn clone(&self) -> Self {
46        Self {
47            id: self.id,
48            kind: self.kind,
49            message: self.message.clone(),
50            context: self.context.clone(),
51            source: self.source.clone(),
52            #[cfg(debug_assertions)]
53            backtrace: std::backtrace::Backtrace::capture(),
54        }
55    }
56}
57
58impl<'de> Deserialize<'de> for Error {
59    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
60    where
61        D: serde::Deserializer<'de>,
62    {
63        #[derive(Deserialize)]
64        struct ErrorData {
65            id: Uuid,
66            kind: ErrorKind,
67            message: String,
68            context: ErrorContext,
69        }
70
71        let data = ErrorData::deserialize(deserializer)?;
72        Ok(Self {
73            id: data.id,
74            kind: data.kind,
75            message: data.message,
76            context: data.context,
77            source: None,
78            #[cfg(debug_assertions)]
79            backtrace: std::backtrace::Backtrace::capture(),
80        })
81    }
82}
83
84/// Error classification for programmatic handling
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
86#[serde(rename_all = "snake_case")]
87pub enum ErrorKind {
88    /// Input validation failed
89    Validation,
90
91    /// Authentication or authorization failed
92    Authentication,
93
94    /// Resource was not found
95    NotFound,
96
97    /// Operation is not permitted
98    PermissionDenied,
99
100    /// Request was malformed or invalid
101    BadRequest,
102
103    /// Server internal error
104    Internal,
105
106    /// Network or transport error
107    Transport,
108
109    /// Serialization/deserialization error
110    Serialization,
111
112    /// Protocol violation or incompatibility
113    Protocol,
114
115    /// Operation timed out
116    Timeout,
117
118    /// Resource is temporarily unavailable
119    Unavailable,
120
121    /// Rate limit exceeded
122    RateLimited,
123
124    /// Configuration error
125    Configuration,
126
127    /// External dependency failed
128    ExternalService,
129
130    /// Operation was cancelled
131    Cancelled,
132}
133
134/// Rich contextual information for errors
135#[derive(Debug, Clone, Default, Serialize, Deserialize)]
136pub struct ErrorContext {
137    /// Operation that was being performed
138    pub operation: Option<String>,
139
140    /// Component where error occurred
141    pub component: Option<String>,
142
143    /// Request ID for tracing
144    pub request_id: Option<String>,
145
146    /// User ID (if applicable)
147    pub user_id: Option<String>,
148
149    /// Additional metadata
150    pub metadata: HashMap<String, serde_json::Value>,
151
152    /// Timestamp when error occurred
153    pub timestamp: chrono::DateTime<chrono::Utc>,
154
155    /// Retry information
156    pub retry_info: Option<RetryInfo>,
157}
158
159/// Information about retry attempts
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct RetryInfo {
162    /// Number of attempts made
163    pub attempts: u32,
164
165    /// Maximum attempts allowed
166    pub max_attempts: u32,
167
168    /// Next retry delay in milliseconds
169    pub retry_after_ms: Option<u64>,
170}
171
172impl Error {
173    /// Create a new error with the specified kind and message
174    pub fn new(kind: ErrorKind, message: impl Into<String>) -> Box<Self> {
175        Box::new(Self {
176            id: Uuid::new_v4(),
177            kind,
178            message: message.into(),
179            context: ErrorContext {
180                timestamp: chrono::Utc::now(),
181                ..Default::default()
182            },
183            source: None,
184            #[cfg(debug_assertions)]
185            backtrace: std::backtrace::Backtrace::capture(),
186        })
187    }
188
189    /// Create a validation error
190    pub fn validation(message: impl Into<String>) -> Box<Self> {
191        Self::new(ErrorKind::Validation, message)
192    }
193
194    /// Create an authentication error
195    pub fn authentication(message: impl Into<String>) -> Box<Self> {
196        Self::new(ErrorKind::Authentication, message)
197    }
198
199    /// Create a not found error
200    pub fn not_found(message: impl Into<String>) -> Box<Self> {
201        Self::new(ErrorKind::NotFound, message)
202    }
203
204    /// Create a permission denied error
205    pub fn permission_denied(message: impl Into<String>) -> Box<Self> {
206        Self::new(ErrorKind::PermissionDenied, message)
207    }
208
209    /// Create a bad request error
210    pub fn bad_request(message: impl Into<String>) -> Box<Self> {
211        Self::new(ErrorKind::BadRequest, message)
212    }
213
214    /// Create an internal error
215    pub fn internal(message: impl Into<String>) -> Box<Self> {
216        Self::new(ErrorKind::Internal, message)
217    }
218
219    /// Create a transport error
220    pub fn transport(message: impl Into<String>) -> Box<Self> {
221        Self::new(ErrorKind::Transport, message)
222    }
223
224    /// Create a serialization error
225    pub fn serialization(message: impl Into<String>) -> Box<Self> {
226        Self::new(ErrorKind::Serialization, message)
227    }
228
229    /// Create a protocol error
230    pub fn protocol(message: impl Into<String>) -> Box<Self> {
231        Self::new(ErrorKind::Protocol, message)
232    }
233
234    /// Create a JSON-RPC error
235    #[must_use]
236    pub fn rpc(code: i32, message: &str) -> Box<Self> {
237        Self::new(ErrorKind::Protocol, format!("RPC error {code}: {message}"))
238    }
239
240    /// Create a timeout error
241    pub fn timeout(message: impl Into<String>) -> Box<Self> {
242        Self::new(ErrorKind::Timeout, message)
243    }
244
245    /// Create an unavailable error
246    pub fn unavailable(message: impl Into<String>) -> Box<Self> {
247        Self::new(ErrorKind::Unavailable, message)
248    }
249
250    /// Create a rate limited error
251    pub fn rate_limited(message: impl Into<String>) -> Box<Self> {
252        Self::new(ErrorKind::RateLimited, message)
253    }
254
255    /// Create a configuration error
256    pub fn configuration(message: impl Into<String>) -> Box<Self> {
257        Self::new(ErrorKind::Configuration, message)
258    }
259
260    /// Create an external service error
261    pub fn external_service(message: impl Into<String>) -> Box<Self> {
262        Self::new(ErrorKind::ExternalService, message)
263    }
264
265    /// Create a cancelled error
266    pub fn cancelled(message: impl Into<String>) -> Box<Self> {
267        Self::new(ErrorKind::Cancelled, message)
268    }
269
270    /// Add context to this error
271    #[must_use]
272    pub fn with_context(
273        mut self: Box<Self>,
274        key: impl Into<String>,
275        value: impl Into<serde_json::Value>,
276    ) -> Box<Self> {
277        self.context.metadata.insert(key.into(), value.into());
278        self
279    }
280
281    /// Set the operation being performed
282    #[must_use]
283    pub fn with_operation(mut self: Box<Self>, operation: impl Into<String>) -> Box<Self> {
284        self.context.operation = Some(operation.into());
285        self
286    }
287
288    /// Set the component where error occurred
289    #[must_use]
290    pub fn with_component(mut self: Box<Self>, component: impl Into<String>) -> Box<Self> {
291        self.context.component = Some(component.into());
292        self
293    }
294
295    /// Set the request ID for tracing
296    #[must_use]
297    pub fn with_request_id(mut self: Box<Self>, request_id: impl Into<String>) -> Box<Self> {
298        self.context.request_id = Some(request_id.into());
299        self
300    }
301
302    /// Set the user ID
303    #[must_use]
304    pub fn with_user_id(mut self: Box<Self>, user_id: impl Into<String>) -> Box<Self> {
305        self.context.user_id = Some(user_id.into());
306        self
307    }
308
309    /// Add retry information
310    #[must_use]
311    pub fn with_retry_info(mut self: Box<Self>, retry_info: RetryInfo) -> Box<Self> {
312        self.context.retry_info = Some(retry_info);
313        self
314    }
315
316    /// Chain this error with a source error
317    #[must_use]
318    pub fn with_source(mut self: Box<Self>, source: Box<Self>) -> Box<Self> {
319        self.source = Some(source);
320        self
321    }
322
323    /// Check if this error is retryable based on its kind
324    pub const fn is_retryable(&self) -> bool {
325        matches!(
326            self.kind,
327            ErrorKind::Timeout
328                | ErrorKind::Unavailable
329                | ErrorKind::Transport
330                | ErrorKind::ExternalService
331                | ErrorKind::RateLimited
332        )
333    }
334
335    /// Check if this error indicates a temporary failure
336    pub const fn is_temporary(&self) -> bool {
337        matches!(
338            self.kind,
339            ErrorKind::Timeout
340                | ErrorKind::Unavailable
341                | ErrorKind::RateLimited
342                | ErrorKind::ExternalService
343        )
344    }
345
346    /// Get the HTTP status code equivalent for this error
347    pub const fn http_status_code(&self) -> u16 {
348        match self.kind {
349            ErrorKind::Validation | ErrorKind::BadRequest => 400,
350            ErrorKind::Authentication => 401,
351            ErrorKind::PermissionDenied => 403,
352            ErrorKind::NotFound => 404,
353            ErrorKind::Timeout => 408,
354            ErrorKind::RateLimited => 429,
355            ErrorKind::Internal
356            | ErrorKind::Configuration
357            | ErrorKind::Serialization
358            | ErrorKind::Protocol => 500,
359            ErrorKind::Transport | ErrorKind::ExternalService | ErrorKind::Unavailable => 503,
360            ErrorKind::Cancelled => 499, // Client closed request
361        }
362    }
363
364    /// Convert to a JSON-RPC error code
365    pub const fn jsonrpc_error_code(&self) -> i32 {
366        match self.kind {
367            ErrorKind::BadRequest | ErrorKind::Validation => -32600, // Invalid Request
368            ErrorKind::Protocol => -32601,                           // Method not found
369            ErrorKind::Serialization => -32602,                      // Invalid params
370            ErrorKind::Internal => -32603,                           // Internal error
371            ErrorKind::NotFound => -32001,                           // Custom: Not found
372            ErrorKind::Authentication => -32002, // Custom: Authentication failed
373            ErrorKind::PermissionDenied => -32003, // Custom: Permission denied
374            ErrorKind::Timeout => -32004,        // Custom: Timeout
375            ErrorKind::Unavailable => -32005,    // Custom: Service unavailable
376            ErrorKind::RateLimited => -32006,    // Custom: Rate limited
377            ErrorKind::Transport => -32007,      // Custom: Transport error
378            ErrorKind::Configuration => -32008,  // Custom: Configuration error
379            ErrorKind::ExternalService => -32009, // Custom: External service error
380            ErrorKind::Cancelled => -32010,      // Custom: Operation cancelled
381        }
382    }
383}
384
385impl fmt::Display for Error {
386    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
387        write!(f, "{}", self.message)?;
388
389        if let Some(operation) = &self.context.operation {
390            write!(f, " (operation: {operation})")?;
391        }
392
393        if let Some(component) = &self.context.component {
394            write!(f, " (component: {component})")?;
395        }
396
397        if let Some(request_id) = &self.context.request_id {
398            write!(f, " (request_id: {request_id})")?;
399        }
400
401        Ok(())
402    }
403}
404
405impl std::error::Error for Error {
406    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
407        // Note: We can't return the source error because it's also an Error type
408        // which would create infinite recursion. In a real implementation,
409        // we'd need to handle this differently.
410        None
411    }
412}
413
414impl ErrorKind {
415    /// Get a human-readable description of this error kind
416    #[must_use]
417    pub const fn description(self) -> &'static str {
418        match self {
419            Self::Validation => "Input validation failed",
420            Self::Authentication => "Authentication failed",
421            Self::NotFound => "Resource not found",
422            Self::PermissionDenied => "Permission denied",
423            Self::BadRequest => "Bad request",
424            Self::Internal => "Internal server error",
425            Self::Transport => "Transport error",
426            Self::Serialization => "Serialization error",
427            Self::Protocol => "Protocol error",
428            Self::Timeout => "Operation timed out",
429            Self::Unavailable => "Service unavailable",
430            Self::RateLimited => "Rate limit exceeded",
431            Self::Configuration => "Configuration error",
432            Self::ExternalService => "External service error",
433            Self::Cancelled => "Operation cancelled",
434        }
435    }
436}
437
438impl fmt::Display for ErrorKind {
439    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
440        write!(f, "{}", self.description())
441    }
442}
443
444/// Convenience macro for creating errors with context
445#[macro_export]
446macro_rules! mcp_error {
447    ($kind:expr, $message:expr) => {
448        $crate::error::Error::new($kind, $message)
449    };
450    ($kind:expr, $message:expr, $($key:expr => $value:expr),+) => {
451        {
452            let mut error = $crate::error::Error::new($kind, $message);
453            $(
454                error = error.with_context($key, $value);
455            )+
456            error
457        }
458    };
459}
460
461/// Extension trait for adding MCP error context to other error types
462pub trait ErrorExt<T> {
463    /// Convert any error to an MCP error with the specified kind
464    ///
465    /// # Errors
466    ///
467    /// Returns an `Error` with the specified kind and message, preserving the source error context.
468    fn with_mcp_error(self, kind: ErrorKind, message: impl Into<String>) -> Result<T>;
469
470    /// Convert any error to an MCP internal error
471    ///
472    /// # Errors
473    ///
474    /// Returns an `Error` with internal error kind and the provided message.
475    fn with_internal_error(self, message: impl Into<String>) -> Result<T>;
476}
477
478impl<T, E> ErrorExt<T> for std::result::Result<T, E>
479where
480    E: std::error::Error + Send + Sync + 'static,
481{
482    fn with_mcp_error(self, kind: ErrorKind, message: impl Into<String>) -> Result<T> {
483        self.map_err(|e| {
484            Error::new(kind, format!("{}: {}", message.into(), e))
485                .with_context("source_error", e.to_string())
486        })
487    }
488
489    fn with_internal_error(self, message: impl Into<String>) -> Result<T> {
490        self.with_mcp_error(ErrorKind::Internal, message)
491    }
492}
493
494// Implement From for common error types
495impl From<serde_json::Error> for Box<Error> {
496    fn from(err: serde_json::Error) -> Self {
497        Error::serialization(format!("JSON serialization error: {err}"))
498    }
499}
500
501impl From<std::io::Error> for Box<Error> {
502    fn from(err: std::io::Error) -> Self {
503        Error::transport(format!("IO error: {err}"))
504    }
505}
506
507#[cfg(test)]
508mod tests {
509    use super::*;
510
511    #[test]
512    fn test_error_creation() {
513        let error = Error::validation("Invalid input");
514        assert_eq!(error.kind, ErrorKind::Validation);
515        assert_eq!(error.message, "Invalid input");
516    }
517
518    #[test]
519    fn test_error_context() {
520        let error = Error::internal("Something went wrong")
521            .with_operation("test_operation")
522            .with_component("test_component")
523            .with_request_id("req-123")
524            .with_context("key", "value");
525
526        assert_eq!(error.context.operation, Some("test_operation".to_string()));
527        assert_eq!(error.context.component, Some("test_component".to_string()));
528        assert_eq!(error.context.request_id, Some("req-123".to_string()));
529        assert_eq!(
530            error.context.metadata.get("key"),
531            Some(&serde_json::Value::String("value".to_string()))
532        );
533    }
534
535    #[test]
536    fn test_error_properties() {
537        let retryable_error = Error::timeout("Request timed out");
538        assert!(retryable_error.is_retryable());
539        assert!(retryable_error.is_temporary());
540
541        let permanent_error = Error::validation("Invalid data");
542        assert!(!permanent_error.is_retryable());
543        assert!(!permanent_error.is_temporary());
544    }
545
546    #[test]
547    fn test_http_status_codes() {
548        assert_eq!(Error::validation("test").http_status_code(), 400);
549        assert_eq!(Error::not_found("test").http_status_code(), 404);
550        assert_eq!(Error::internal("test").http_status_code(), 500);
551    }
552
553    #[test]
554    fn test_error_macro() {
555        let error = mcp_error!(ErrorKind::Validation, "test message");
556        assert_eq!(error.kind, ErrorKind::Validation);
557        assert_eq!(error.message, "test message");
558
559        let error_with_context = mcp_error!(
560            ErrorKind::Internal,
561            "test message",
562            "key1" => "value1",
563            "key2" => 42
564        );
565        assert_eq!(error_with_context.context.metadata.len(), 2);
566    }
567}