Skip to main content

camel_api/
error.rs

1use std::sync::Arc;
2use thiserror::Error;
3
4/// Core error type for the Camel framework.
5#[derive(Debug, Clone, Error)]
6#[non_exhaustive]
7pub enum CamelError {
8    #[error("Component not found: {0}")]
9    ComponentNotFound(String),
10
11    #[error("Endpoint creation failed: {0}")]
12    EndpointCreationFailed(String),
13
14    #[error("Processor error: {0}")]
15    ProcessorError(String),
16
17    /// Like `ProcessorError` but preserves the source error chain
18    /// for downstream inspection (e.g. via `std::error::Error::source()`).
19    #[error("Processor error: {0}")]
20    ProcessorErrorWithSource(String, #[source] Arc<dyn std::error::Error + Send + Sync>),
21
22    #[error("Type conversion failed: {0}")]
23    TypeConversionFailed(String),
24
25    #[error("Invalid URI: {0}")]
26    InvalidUri(String),
27
28    #[error("Channel closed")]
29    ChannelClosed,
30
31    #[error("Route error: {0}")]
32    RouteError(String),
33
34    #[error("IO error: {0}")]
35    Io(String),
36
37    #[error("Dead letter channel failed: {0}")]
38    DeadLetterChannelFailed(String),
39
40    #[error("Circuit breaker open: {0}")]
41    CircuitOpen(String),
42
43    #[error("HTTP {method} {url} failed: {status_code} {status_text}")]
44    HttpOperationFailed {
45        method: String,
46        url: String,
47        status_code: u16,
48        status_text: String,
49        response_body: Option<String>,
50    },
51
52    #[error("Exchange stopped by Stop EIP")]
53    Stopped,
54
55    #[error("Configuration error: {0}")]
56    Config(String),
57
58    #[error("Body stream has already been consumed")]
59    AlreadyConsumed,
60
61    #[error("Stream size exceeded limit: {0}")]
62    StreamLimitExceeded(usize),
63
64    #[error("Unauthenticated: {0}")]
65    Unauthenticated(String),
66
67    #[error("Unauthorized: {0}")]
68    Unauthorized(String),
69}
70
71impl CamelError {
72    pub fn classify(&self) -> &'static str {
73        #[allow(unreachable_patterns)]
74        match self {
75            Self::ComponentNotFound(_) => "component",
76            Self::EndpointCreationFailed(_) | Self::InvalidUri(_) => "endpoint",
77            Self::ProcessorError(_) | Self::ProcessorErrorWithSource(_, _) => "processor",
78            Self::TypeConversionFailed(_) | Self::AlreadyConsumed => "type_conversion",
79            Self::Io(_) => "io",
80            Self::RouteError(_) => "route",
81            Self::CircuitOpen(_) => "circuit_open",
82            Self::HttpOperationFailed { .. } => "http",
83            Self::Config(_) => "config",
84            Self::DeadLetterChannelFailed(_) => "dead_letter",
85            Self::Stopped => "stopped",
86            Self::StreamLimitExceeded(_) => "stream",
87            Self::ChannelClosed => "channel",
88            Self::Unauthenticated(_) => "unauthenticated",
89            Self::Unauthorized(_) => "unauthorized",
90            _ => "unknown",
91        }
92    }
93}
94
95impl From<std::io::Error> for CamelError {
96    fn from(err: std::io::Error) -> Self {
97        CamelError::Io(err.to_string())
98    }
99}
100
101impl From<crate::template::TemplateError> for CamelError {
102    fn from(err: crate::template::TemplateError) -> Self {
103        CamelError::Config(err.to_string())
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    fn all_error_samples() -> Vec<CamelError> {
112        vec![
113            CamelError::ComponentNotFound("x".to_string()),
114            CamelError::EndpointCreationFailed("x".to_string()),
115            CamelError::ProcessorError("x".to_string()),
116            CamelError::ProcessorErrorWithSource(
117                "x".to_string(),
118                Arc::new(std::io::Error::new(std::io::ErrorKind::Other, "inner")),
119            ),
120            CamelError::TypeConversionFailed("x".to_string()),
121            CamelError::InvalidUri("x".to_string()),
122            CamelError::ChannelClosed,
123            CamelError::RouteError("x".to_string()),
124            CamelError::Io("x".to_string()),
125            CamelError::DeadLetterChannelFailed("x".to_string()),
126            CamelError::CircuitOpen("x".to_string()),
127            CamelError::HttpOperationFailed {
128                method: "GET".to_string(),
129                url: "https://example.com".to_string(),
130                status_code: 500,
131                status_text: "Internal Server Error".to_string(),
132                response_body: Some("error".to_string()),
133            },
134            CamelError::Stopped,
135            CamelError::Config("x".to_string()),
136            CamelError::AlreadyConsumed,
137            CamelError::StreamLimitExceeded(42),
138            CamelError::Unauthenticated("token expired".to_string()),
139            CamelError::Unauthorized("missing admin role".to_string()),
140        ]
141    }
142
143    #[test]
144    fn test_http_operation_failed_display() {
145        let err = CamelError::HttpOperationFailed {
146            method: "GET".to_string(),
147            url: "https://example.com/test".to_string(),
148            status_code: 404,
149            status_text: "Not Found".to_string(),
150            response_body: Some("page not found".to_string()),
151        };
152        let msg = format!("{err}");
153        assert!(msg.contains("404"));
154        assert!(msg.contains("Not Found"));
155    }
156
157    #[test]
158    fn test_http_operation_failed_clone() {
159        let err = CamelError::HttpOperationFailed {
160            method: "POST".to_string(),
161            url: "https://api.example.com/users".to_string(),
162            status_code: 500,
163            status_text: "Internal Server Error".to_string(),
164            response_body: None,
165        };
166        let cloned = err.clone();
167        assert!(matches!(
168            cloned,
169            CamelError::HttpOperationFailed {
170                status_code: 500,
171                ..
172            }
173        ));
174    }
175
176    #[test]
177    fn test_stopped_variant_exists_and_is_clone() {
178        let err = CamelError::Stopped;
179        let cloned = err.clone();
180        assert!(matches!(cloned, CamelError::Stopped));
181        assert_eq!(format!("{err}"), "Exchange stopped by Stop EIP");
182    }
183
184    #[test]
185    fn test_classify_maps_all_variants() {
186        assert_eq!(
187            CamelError::ComponentNotFound("x".to_string()).classify(),
188            "component"
189        );
190        assert_eq!(
191            CamelError::EndpointCreationFailed("x".to_string()).classify(),
192            "endpoint"
193        );
194        assert_eq!(
195            CamelError::ProcessorError("x".to_string()).classify(),
196            "processor"
197        );
198        assert_eq!(
199            CamelError::TypeConversionFailed("x".to_string()).classify(),
200            "type_conversion"
201        );
202        assert_eq!(
203            CamelError::InvalidUri("x".to_string()).classify(),
204            "endpoint"
205        );
206        assert_eq!(CamelError::ChannelClosed.classify(), "channel");
207        assert_eq!(CamelError::RouteError("x".to_string()).classify(), "route");
208        assert_eq!(CamelError::Io("x".to_string()).classify(), "io");
209        assert_eq!(
210            CamelError::DeadLetterChannelFailed("x".to_string()).classify(),
211            "dead_letter"
212        );
213        assert_eq!(
214            CamelError::CircuitOpen("x".to_string()).classify(),
215            "circuit_open"
216        );
217        assert_eq!(
218            CamelError::HttpOperationFailed {
219                method: "GET".to_string(),
220                url: "https://example.com".to_string(),
221                status_code: 500,
222                status_text: "Internal Server Error".to_string(),
223                response_body: None,
224            }
225            .classify(),
226            "http"
227        );
228        assert_eq!(CamelError::Stopped.classify(), "stopped");
229        assert_eq!(CamelError::Config("x".to_string()).classify(), "config");
230        assert_eq!(CamelError::AlreadyConsumed.classify(), "type_conversion");
231        assert_eq!(CamelError::StreamLimitExceeded(42).classify(), "stream");
232    }
233
234    #[test]
235    fn test_classify_output_is_ascii_and_short() {
236        for error in all_error_samples() {
237            let class = error.classify();
238            assert!(class.is_ascii());
239            assert!(class.len() <= 15, "class too long: {class}");
240        }
241    }
242
243    #[test]
244    fn test_auth_variants_classify() {
245        assert_eq!(
246            CamelError::Unauthenticated("x".to_string()).classify(),
247            "unauthenticated"
248        );
249        assert_eq!(
250            CamelError::Unauthorized("x".to_string()).classify(),
251            "unauthorized"
252        );
253    }
254
255    #[test]
256    fn test_auth_variants_are_clone() {
257        let err = CamelError::Unauthenticated("test".to_string());
258        let cloned = err.clone();
259        assert!(matches!(cloned, CamelError::Unauthenticated(_)));
260
261        let err2 = CamelError::Unauthorized("test".to_string());
262        let cloned2 = err2.clone();
263        assert!(matches!(cloned2, CamelError::Unauthorized(_)));
264    }
265}