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
65impl CamelError {
66    pub fn classify(&self) -> &'static str {
67        #[allow(unreachable_patterns)]
68        match self {
69            Self::ComponentNotFound(_) => "component",
70            Self::EndpointCreationFailed(_) | Self::InvalidUri(_) => "endpoint",
71            Self::ProcessorError(_) | Self::ProcessorErrorWithSource(_, _) => "processor",
72            Self::TypeConversionFailed(_) | Self::AlreadyConsumed => "type_conversion",
73            Self::Io(_) => "io",
74            Self::RouteError(_) => "route",
75            Self::CircuitOpen(_) => "circuit_open",
76            Self::HttpOperationFailed { .. } => "http",
77            Self::Config(_) => "config",
78            Self::DeadLetterChannelFailed(_) => "dead_letter",
79            Self::Stopped => "stopped",
80            Self::StreamLimitExceeded(_) => "stream",
81            Self::ChannelClosed => "channel",
82            _ => "unknown",
83        }
84    }
85}
86
87impl From<std::io::Error> for CamelError {
88    fn from(err: std::io::Error) -> Self {
89        CamelError::Io(err.to_string())
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    fn all_error_samples() -> Vec<CamelError> {
98        vec![
99            CamelError::ComponentNotFound("x".to_string()),
100            CamelError::EndpointCreationFailed("x".to_string()),
101            CamelError::ProcessorError("x".to_string()),
102            CamelError::ProcessorErrorWithSource(
103                "x".to_string(),
104                Arc::new(std::io::Error::new(std::io::ErrorKind::Other, "inner")),
105            ),
106            CamelError::TypeConversionFailed("x".to_string()),
107            CamelError::InvalidUri("x".to_string()),
108            CamelError::ChannelClosed,
109            CamelError::RouteError("x".to_string()),
110            CamelError::Io("x".to_string()),
111            CamelError::DeadLetterChannelFailed("x".to_string()),
112            CamelError::CircuitOpen("x".to_string()),
113            CamelError::HttpOperationFailed {
114                method: "GET".to_string(),
115                url: "https://example.com".to_string(),
116                status_code: 500,
117                status_text: "Internal Server Error".to_string(),
118                response_body: Some("error".to_string()),
119            },
120            CamelError::Stopped,
121            CamelError::Config("x".to_string()),
122            CamelError::AlreadyConsumed,
123            CamelError::StreamLimitExceeded(42),
124        ]
125    }
126
127    #[test]
128    fn test_http_operation_failed_display() {
129        let err = CamelError::HttpOperationFailed {
130            method: "GET".to_string(),
131            url: "https://example.com/test".to_string(),
132            status_code: 404,
133            status_text: "Not Found".to_string(),
134            response_body: Some("page not found".to_string()),
135        };
136        let msg = format!("{err}");
137        assert!(msg.contains("404"));
138        assert!(msg.contains("Not Found"));
139    }
140
141    #[test]
142    fn test_http_operation_failed_clone() {
143        let err = CamelError::HttpOperationFailed {
144            method: "POST".to_string(),
145            url: "https://api.example.com/users".to_string(),
146            status_code: 500,
147            status_text: "Internal Server Error".to_string(),
148            response_body: None,
149        };
150        let cloned = err.clone();
151        assert!(matches!(
152            cloned,
153            CamelError::HttpOperationFailed {
154                status_code: 500,
155                ..
156            }
157        ));
158    }
159
160    #[test]
161    fn test_stopped_variant_exists_and_is_clone() {
162        let err = CamelError::Stopped;
163        let cloned = err.clone();
164        assert!(matches!(cloned, CamelError::Stopped));
165        assert_eq!(format!("{err}"), "Exchange stopped by Stop EIP");
166    }
167
168    #[test]
169    fn test_classify_maps_all_variants() {
170        assert_eq!(
171            CamelError::ComponentNotFound("x".to_string()).classify(),
172            "component"
173        );
174        assert_eq!(
175            CamelError::EndpointCreationFailed("x".to_string()).classify(),
176            "endpoint"
177        );
178        assert_eq!(
179            CamelError::ProcessorError("x".to_string()).classify(),
180            "processor"
181        );
182        assert_eq!(
183            CamelError::TypeConversionFailed("x".to_string()).classify(),
184            "type_conversion"
185        );
186        assert_eq!(
187            CamelError::InvalidUri("x".to_string()).classify(),
188            "endpoint"
189        );
190        assert_eq!(CamelError::ChannelClosed.classify(), "channel");
191        assert_eq!(CamelError::RouteError("x".to_string()).classify(), "route");
192        assert_eq!(CamelError::Io("x".to_string()).classify(), "io");
193        assert_eq!(
194            CamelError::DeadLetterChannelFailed("x".to_string()).classify(),
195            "dead_letter"
196        );
197        assert_eq!(
198            CamelError::CircuitOpen("x".to_string()).classify(),
199            "circuit_open"
200        );
201        assert_eq!(
202            CamelError::HttpOperationFailed {
203                method: "GET".to_string(),
204                url: "https://example.com".to_string(),
205                status_code: 500,
206                status_text: "Internal Server Error".to_string(),
207                response_body: None,
208            }
209            .classify(),
210            "http"
211        );
212        assert_eq!(CamelError::Stopped.classify(), "stopped");
213        assert_eq!(CamelError::Config("x".to_string()).classify(), "config");
214        assert_eq!(CamelError::AlreadyConsumed.classify(), "type_conversion");
215        assert_eq!(CamelError::StreamLimitExceeded(42).classify(), "stream");
216    }
217
218    #[test]
219    fn test_classify_output_is_ascii_and_short() {
220        for error in all_error_samples() {
221            let class = error.classify();
222            assert!(class.is_ascii());
223            assert!(class.len() <= 15, "class too long: {class}");
224        }
225    }
226}