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    /// Handler execution error
134    Handler,
135}
136
137/// Rich contextual information for errors
138#[derive(Debug, Clone, Default, Serialize, Deserialize)]
139pub struct ErrorContext {
140    /// Operation that was being performed
141    pub operation: Option<String>,
142
143    /// Component where error occurred
144    pub component: Option<String>,
145
146    /// Request ID for tracing
147    pub request_id: Option<String>,
148
149    /// User ID (if applicable)
150    pub user_id: Option<String>,
151
152    /// Additional metadata
153    pub metadata: HashMap<String, serde_json::Value>,
154
155    /// Timestamp when error occurred
156    pub timestamp: chrono::DateTime<chrono::Utc>,
157
158    /// Retry information
159    pub retry_info: Option<RetryInfo>,
160}
161
162/// Information about retry attempts
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct RetryInfo {
165    /// Number of attempts made
166    pub attempts: u32,
167
168    /// Maximum attempts allowed
169    pub max_attempts: u32,
170
171    /// Next retry delay in milliseconds
172    pub retry_after_ms: Option<u64>,
173}
174
175impl Error {
176    /// Create a new error with the specified kind and message
177    pub fn new(kind: ErrorKind, message: impl Into<String>) -> Box<Self> {
178        Box::new(Self {
179            id: Uuid::new_v4(),
180            kind,
181            message: message.into(),
182            context: ErrorContext {
183                timestamp: chrono::Utc::now(),
184                ..Default::default()
185            },
186            source: None,
187            #[cfg(debug_assertions)]
188            backtrace: std::backtrace::Backtrace::capture(),
189        })
190    }
191
192    /// Create a validation error
193    pub fn validation(message: impl Into<String>) -> Box<Self> {
194        Self::new(ErrorKind::Validation, message)
195    }
196
197    /// Create an authentication error
198    pub fn authentication(message: impl Into<String>) -> Box<Self> {
199        Self::new(ErrorKind::Authentication, message)
200    }
201
202    /// Create a not found error
203    pub fn not_found(message: impl Into<String>) -> Box<Self> {
204        Self::new(ErrorKind::NotFound, message)
205    }
206
207    /// Create a permission denied error
208    pub fn permission_denied(message: impl Into<String>) -> Box<Self> {
209        Self::new(ErrorKind::PermissionDenied, message)
210    }
211
212    /// Create a bad request error
213    pub fn bad_request(message: impl Into<String>) -> Box<Self> {
214        Self::new(ErrorKind::BadRequest, message)
215    }
216
217    /// Create an internal error
218    pub fn internal(message: impl Into<String>) -> Box<Self> {
219        Self::new(ErrorKind::Internal, message)
220    }
221
222    /// Create a transport error
223    pub fn transport(message: impl Into<String>) -> Box<Self> {
224        Self::new(ErrorKind::Transport, message)
225    }
226
227    /// Create a serialization error
228    pub fn serialization(message: impl Into<String>) -> Box<Self> {
229        Self::new(ErrorKind::Serialization, message)
230    }
231
232    /// Create a protocol error
233    pub fn protocol(message: impl Into<String>) -> Box<Self> {
234        Self::new(ErrorKind::Protocol, message)
235    }
236
237    /// Create a JSON-RPC error
238    #[must_use]
239    pub fn rpc(code: i32, message: &str) -> Box<Self> {
240        Self::new(ErrorKind::Protocol, format!("RPC error {code}: {message}"))
241    }
242
243    /// Create a timeout error
244    pub fn timeout(message: impl Into<String>) -> Box<Self> {
245        Self::new(ErrorKind::Timeout, message)
246    }
247
248    /// Create an unavailable error
249    pub fn unavailable(message: impl Into<String>) -> Box<Self> {
250        Self::new(ErrorKind::Unavailable, message)
251    }
252
253    /// Create a rate limited error
254    pub fn rate_limited(message: impl Into<String>) -> Box<Self> {
255        Self::new(ErrorKind::RateLimited, message)
256    }
257
258    /// Create a configuration error
259    pub fn configuration(message: impl Into<String>) -> Box<Self> {
260        Self::new(ErrorKind::Configuration, message)
261    }
262
263    /// Create an external service error
264    pub fn external_service(message: impl Into<String>) -> Box<Self> {
265        Self::new(ErrorKind::ExternalService, message)
266    }
267
268    /// Create a cancelled error
269    pub fn cancelled(message: impl Into<String>) -> Box<Self> {
270        Self::new(ErrorKind::Cancelled, message)
271    }
272
273    /// Create a handler error - for compatibility with macro-generated code
274    pub fn handler(message: impl Into<String>) -> Box<Self> {
275        Self::new(ErrorKind::Handler, message)
276    }
277
278    /// Add context to this error
279    #[must_use]
280    pub fn with_context(
281        mut self: Box<Self>,
282        key: impl Into<String>,
283        value: impl Into<serde_json::Value>,
284    ) -> Box<Self> {
285        self.context.metadata.insert(key.into(), value.into());
286        self
287    }
288
289    /// Set the operation being performed
290    #[must_use]
291    pub fn with_operation(mut self: Box<Self>, operation: impl Into<String>) -> Box<Self> {
292        self.context.operation = Some(operation.into());
293        self
294    }
295
296    /// Set the component where error occurred
297    #[must_use]
298    pub fn with_component(mut self: Box<Self>, component: impl Into<String>) -> Box<Self> {
299        self.context.component = Some(component.into());
300        self
301    }
302
303    /// Set the request ID for tracing
304    #[must_use]
305    pub fn with_request_id(mut self: Box<Self>, request_id: impl Into<String>) -> Box<Self> {
306        self.context.request_id = Some(request_id.into());
307        self
308    }
309
310    /// Set the user ID
311    #[must_use]
312    pub fn with_user_id(mut self: Box<Self>, user_id: impl Into<String>) -> Box<Self> {
313        self.context.user_id = Some(user_id.into());
314        self
315    }
316
317    /// Add retry information
318    #[must_use]
319    pub fn with_retry_info(mut self: Box<Self>, retry_info: RetryInfo) -> Box<Self> {
320        self.context.retry_info = Some(retry_info);
321        self
322    }
323
324    /// Chain this error with a source error
325    #[must_use]
326    pub fn with_source(mut self: Box<Self>, source: Box<Self>) -> Box<Self> {
327        self.source = Some(source);
328        self
329    }
330
331    /// Check if this error is retryable based on its kind
332    pub const fn is_retryable(&self) -> bool {
333        matches!(
334            self.kind,
335            ErrorKind::Timeout
336                | ErrorKind::Unavailable
337                | ErrorKind::Transport
338                | ErrorKind::ExternalService
339                | ErrorKind::RateLimited
340        )
341    }
342
343    /// Check if this error indicates a temporary failure
344    pub const fn is_temporary(&self) -> bool {
345        matches!(
346            self.kind,
347            ErrorKind::Timeout
348                | ErrorKind::Unavailable
349                | ErrorKind::RateLimited
350                | ErrorKind::ExternalService
351        )
352    }
353
354    /// Get the HTTP status code equivalent for this error
355    pub const fn http_status_code(&self) -> u16 {
356        match self.kind {
357            ErrorKind::Validation | ErrorKind::BadRequest => 400,
358            ErrorKind::Authentication => 401,
359            ErrorKind::PermissionDenied => 403,
360            ErrorKind::NotFound => 404,
361            ErrorKind::Timeout => 408,
362            ErrorKind::RateLimited => 429,
363            ErrorKind::Internal
364            | ErrorKind::Configuration
365            | ErrorKind::Serialization
366            | ErrorKind::Protocol
367            | ErrorKind::Handler => 500,
368            ErrorKind::Transport | ErrorKind::ExternalService | ErrorKind::Unavailable => 503,
369            ErrorKind::Cancelled => 499, // Client closed request
370        }
371    }
372
373    /// Convert to a JSON-RPC error code
374    pub const fn jsonrpc_error_code(&self) -> i32 {
375        match self.kind {
376            ErrorKind::BadRequest | ErrorKind::Validation => -32600, // Invalid Request
377            ErrorKind::Protocol => -32601,                           // Method not found
378            ErrorKind::Serialization => -32602,                      // Invalid params
379            ErrorKind::Internal => -32603,                           // Internal error
380            ErrorKind::NotFound => -32001,                           // Custom: Not found
381            ErrorKind::Authentication => -32002, // Custom: Authentication failed
382            ErrorKind::PermissionDenied => -32003, // Custom: Permission denied
383            ErrorKind::Timeout => -32004,        // Custom: Timeout
384            ErrorKind::Unavailable => -32005,    // Custom: Service unavailable
385            ErrorKind::RateLimited => -32006,    // Custom: Rate limited
386            ErrorKind::Transport => -32007,      // Custom: Transport error
387            ErrorKind::Configuration => -32008,  // Custom: Configuration error
388            ErrorKind::ExternalService => -32009, // Custom: External service error
389            ErrorKind::Cancelled => -32010,      // Custom: Operation cancelled
390            ErrorKind::Handler => -32011,        // Custom: Handler error
391        }
392    }
393}
394
395impl fmt::Display for Error {
396    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
397        write!(f, "{}", self.message)?;
398
399        if let Some(operation) = &self.context.operation {
400            write!(f, " (operation: {operation})")?;
401        }
402
403        if let Some(component) = &self.context.component {
404            write!(f, " (component: {component})")?;
405        }
406
407        if let Some(request_id) = &self.context.request_id {
408            write!(f, " (request_id: {request_id})")?;
409        }
410
411        Ok(())
412    }
413}
414
415impl std::error::Error for Error {
416    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
417        // Note: We can't return the source error because it's also an Error type
418        // which would create infinite recursion. Current design returns None to avoid this.
419        // Future enhancement could add proper error chaining with boxed std::error::Error.
420        None
421    }
422}
423
424impl ErrorKind {
425    /// Get a human-readable description of this error kind
426    #[must_use]
427    pub const fn description(self) -> &'static str {
428        match self {
429            Self::Validation => "Input validation failed",
430            Self::Authentication => "Authentication failed",
431            Self::NotFound => "Resource not found",
432            Self::PermissionDenied => "Permission denied",
433            Self::BadRequest => "Bad request",
434            Self::Internal => "Internal server error",
435            Self::Transport => "Transport error",
436            Self::Serialization => "Serialization error",
437            Self::Protocol => "Protocol error",
438            Self::Timeout => "Operation timed out",
439            Self::Unavailable => "Service unavailable",
440            Self::RateLimited => "Rate limit exceeded",
441            Self::Configuration => "Configuration error",
442            Self::ExternalService => "External service error",
443            Self::Cancelled => "Operation cancelled",
444            Self::Handler => "Handler execution error",
445        }
446    }
447}
448
449impl fmt::Display for ErrorKind {
450    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
451        write!(f, "{}", self.description())
452    }
453}
454
455/// Convenience macro for creating errors with context
456#[macro_export]
457macro_rules! mcp_error {
458    ($kind:expr, $message:expr) => {
459        $crate::error::Error::new($kind, $message)
460    };
461    ($kind:expr, $message:expr, $($key:expr => $value:expr),+) => {
462        {
463            let mut error = $crate::error::Error::new($kind, $message);
464            $(
465                error = error.with_context($key, $value);
466            )+
467            error
468        }
469    };
470}
471
472/// Extension trait for adding MCP error context to other error types
473pub trait ErrorExt<T> {
474    /// Convert any error to an MCP error with the specified kind
475    ///
476    /// # Errors
477    ///
478    /// Returns an `Error` with the specified kind and message, preserving the source error context.
479    fn with_mcp_error(self, kind: ErrorKind, message: impl Into<String>) -> Result<T>;
480
481    /// Convert any error to an MCP internal error
482    ///
483    /// # Errors
484    ///
485    /// Returns an `Error` with internal error kind and the provided message.
486    fn with_internal_error(self, message: impl Into<String>) -> Result<T>;
487}
488
489impl<T, E> ErrorExt<T> for std::result::Result<T, E>
490where
491    E: std::error::Error + Send + Sync + 'static,
492{
493    fn with_mcp_error(self, kind: ErrorKind, message: impl Into<String>) -> Result<T> {
494        self.map_err(|e| {
495            Error::new(kind, format!("{}: {}", message.into(), e))
496                .with_context("source_error", e.to_string())
497        })
498    }
499
500    fn with_internal_error(self, message: impl Into<String>) -> Result<T> {
501        self.with_mcp_error(ErrorKind::Internal, message)
502    }
503}
504
505// Implement From for common error types
506impl From<serde_json::Error> for Box<Error> {
507    fn from(err: serde_json::Error) -> Self {
508        Error::serialization(format!("JSON serialization error: {err}"))
509    }
510}
511
512impl From<std::io::Error> for Box<Error> {
513    fn from(err: std::io::Error) -> Self {
514        Error::transport(format!("IO error: {err}"))
515    }
516}
517
518#[cfg(test)]
519mod tests {
520    use super::*;
521
522    #[test]
523    fn test_error_creation() {
524        let error = Error::validation("Invalid input");
525        assert_eq!(error.kind, ErrorKind::Validation);
526        assert_eq!(error.message, "Invalid input");
527    }
528
529    #[test]
530    fn test_error_context() {
531        let error = Error::internal("Something went wrong")
532            .with_operation("test_operation")
533            .with_component("test_component")
534            .with_request_id("req-123")
535            .with_context("key", "value");
536
537        assert_eq!(error.context.operation, Some("test_operation".to_string()));
538        assert_eq!(error.context.component, Some("test_component".to_string()));
539        assert_eq!(error.context.request_id, Some("req-123".to_string()));
540        assert_eq!(
541            error.context.metadata.get("key"),
542            Some(&serde_json::Value::String("value".to_string()))
543        );
544    }
545
546    #[test]
547    fn test_error_properties() {
548        let retryable_error = Error::timeout("Request timed out");
549        assert!(retryable_error.is_retryable());
550        assert!(retryable_error.is_temporary());
551
552        let permanent_error = Error::validation("Invalid data");
553        assert!(!permanent_error.is_retryable());
554        assert!(!permanent_error.is_temporary());
555    }
556
557    #[test]
558    fn test_http_status_codes() {
559        assert_eq!(Error::validation("test").http_status_code(), 400);
560        assert_eq!(Error::not_found("test").http_status_code(), 404);
561        assert_eq!(Error::internal("test").http_status_code(), 500);
562    }
563
564    #[test]
565    fn test_error_macro() {
566        let error = mcp_error!(ErrorKind::Validation, "test message");
567        assert_eq!(error.kind, ErrorKind::Validation);
568        assert_eq!(error.message, "test message");
569
570        let error_with_context = mcp_error!(
571            ErrorKind::Internal,
572            "test message",
573            "key1" => "value1",
574            "key2" => 42
575        );
576        assert_eq!(error_with_context.context.metadata.len(), 2);
577    }
578}