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    /// Producer's `poll_ready` returned a shutdown signal — the consumer/semaphore
56    /// is closing and the producer cannot acquire a permit. Distinct from Stop EIP
57    /// (which is successful control flow). Used by JMS/OpenSearch producers. See ADR-0024.
58    #[error("Consumer stopping: semaphore closed during poll_ready")]
59    ConsumerStopping,
60
61    #[error("Configuration error: {0}")]
62    Config(String),
63
64    #[error("Body stream has already been consumed")]
65    AlreadyConsumed,
66
67    #[error("Stream size exceeded limit: {0}")]
68    StreamLimitExceeded(usize),
69
70    #[error("Unauthenticated: {0}")]
71    Unauthenticated(String),
72
73    #[error("Unauthorized: {0}")]
74    Unauthorized(String),
75}
76
77impl CamelError {
78    pub fn classify(&self) -> &'static str {
79        #[allow(unreachable_patterns)]
80        match self {
81            Self::ComponentNotFound(_) => "component",
82            Self::EndpointCreationFailed(_) | Self::InvalidUri(_) => "endpoint",
83            Self::ProcessorError(_) | Self::ProcessorErrorWithSource(_, _) => "processor",
84            Self::TypeConversionFailed(_) | Self::AlreadyConsumed => "type_conversion",
85            Self::Io(_) => "io",
86            Self::RouteError(_) => "route",
87            Self::CircuitOpen(_) => "circuit_open",
88            Self::HttpOperationFailed { .. } => "http",
89            Self::Config(_) => "config",
90            Self::DeadLetterChannelFailed(_) => "dead_letter",
91            Self::Stopped => "stopped",
92            Self::ConsumerStopping => "consumer_stop",
93            Self::StreamLimitExceeded(_) => "stream",
94            Self::ChannelClosed => "channel",
95            Self::Unauthenticated(_) => "unauthenticated",
96            Self::Unauthorized(_) => "unauthorized",
97            _ => "unknown",
98        }
99    }
100
101    /// Stable variant name used by `doTry` catch-by-variant matchers.
102    ///
103    /// `ProcessorErrorWithSource` aliases to `"ProcessorError"` — the two variants are
104    /// not distinguishable by name in MVP (see spec §5.4).
105    ///
106    /// The enum is `#[non_exhaustive]`; this match lives in the defining crate (camel-api),
107    /// so internal exhaustive matching is allowed. Adding a new variant without updating
108    /// this method will fail to compile, surfaced by `variant_name_tests`.
109    pub fn variant_name(&self) -> &'static str {
110        match self {
111            Self::ComponentNotFound(_) => "ComponentNotFound",
112            Self::EndpointCreationFailed(_) => "EndpointCreationFailed",
113            Self::ProcessorError(_) => "ProcessorError",
114            Self::ProcessorErrorWithSource(_, _) => "ProcessorError",
115            Self::TypeConversionFailed(_) => "TypeConversionFailed",
116            Self::InvalidUri(_) => "InvalidUri",
117            Self::ChannelClosed => "ChannelClosed",
118            Self::RouteError(_) => "RouteError",
119            Self::Io(_) => "Io",
120            Self::DeadLetterChannelFailed(_) => "DeadLetterChannelFailed",
121            Self::CircuitOpen(_) => "CircuitOpen",
122            Self::HttpOperationFailed { .. } => "HttpOperationFailed",
123            Self::Stopped => "Stopped",
124            Self::ConsumerStopping => "ConsumerStopping",
125            Self::Config(_) => "Config",
126            Self::AlreadyConsumed => "AlreadyConsumed",
127            Self::StreamLimitExceeded(_) => "StreamLimitExceeded",
128            Self::Unauthenticated(_) => "Unauthenticated",
129            Self::Unauthorized(_) => "Unauthorized",
130        }
131    }
132}
133
134impl From<std::io::Error> for CamelError {
135    fn from(err: std::io::Error) -> Self {
136        CamelError::Io(err.to_string())
137    }
138}
139
140impl From<crate::template::TemplateError> for CamelError {
141    fn from(err: crate::template::TemplateError) -> Self {
142        CamelError::Config(err.to_string())
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    fn all_error_samples() -> Vec<CamelError> {
151        vec![
152            CamelError::ComponentNotFound("x".to_string()),
153            CamelError::EndpointCreationFailed("x".to_string()),
154            CamelError::ProcessorError("x".to_string()),
155            CamelError::ProcessorErrorWithSource(
156                "x".to_string(),
157                Arc::new(std::io::Error::new(std::io::ErrorKind::Other, "inner")),
158            ),
159            CamelError::TypeConversionFailed("x".to_string()),
160            CamelError::InvalidUri("x".to_string()),
161            CamelError::ChannelClosed,
162            CamelError::RouteError("x".to_string()),
163            CamelError::Io("x".to_string()),
164            CamelError::DeadLetterChannelFailed("x".to_string()),
165            CamelError::CircuitOpen("x".to_string()),
166            CamelError::HttpOperationFailed {
167                method: "GET".to_string(),
168                url: "https://example.com".to_string(),
169                status_code: 500,
170                status_text: "Internal Server Error".to_string(),
171                response_body: Some("error".to_string()),
172            },
173            CamelError::Stopped,
174            CamelError::ConsumerStopping,
175            CamelError::Config("x".to_string()),
176            CamelError::AlreadyConsumed,
177            CamelError::StreamLimitExceeded(42),
178            CamelError::Unauthenticated("token expired".to_string()),
179            CamelError::Unauthorized("missing admin role".to_string()),
180        ]
181    }
182
183    #[test]
184    fn test_http_operation_failed_display() {
185        let err = CamelError::HttpOperationFailed {
186            method: "GET".to_string(),
187            url: "https://example.com/test".to_string(),
188            status_code: 404,
189            status_text: "Not Found".to_string(),
190            response_body: Some("page not found".to_string()),
191        };
192        let msg = format!("{err}");
193        assert!(msg.contains("404"));
194        assert!(msg.contains("Not Found"));
195    }
196
197    #[test]
198    fn test_http_operation_failed_clone() {
199        let err = CamelError::HttpOperationFailed {
200            method: "POST".to_string(),
201            url: "https://api.example.com/users".to_string(),
202            status_code: 500,
203            status_text: "Internal Server Error".to_string(),
204            response_body: None,
205        };
206        let cloned = err.clone();
207        assert!(matches!(
208            cloned,
209            CamelError::HttpOperationFailed {
210                status_code: 500,
211                ..
212            }
213        ));
214    }
215
216    #[test]
217    fn test_stopped_variant_exists_and_is_clone() {
218        let err = CamelError::Stopped;
219        let cloned = err.clone();
220        assert!(matches!(cloned, CamelError::Stopped));
221        assert_eq!(format!("{err}"), "Exchange stopped by Stop EIP");
222    }
223
224    #[test]
225    fn test_classify_maps_all_variants() {
226        assert_eq!(
227            CamelError::ComponentNotFound("x".to_string()).classify(),
228            "component"
229        );
230        assert_eq!(
231            CamelError::EndpointCreationFailed("x".to_string()).classify(),
232            "endpoint"
233        );
234        assert_eq!(
235            CamelError::ProcessorError("x".to_string()).classify(),
236            "processor"
237        );
238        assert_eq!(
239            CamelError::TypeConversionFailed("x".to_string()).classify(),
240            "type_conversion"
241        );
242        assert_eq!(
243            CamelError::InvalidUri("x".to_string()).classify(),
244            "endpoint"
245        );
246        assert_eq!(CamelError::ChannelClosed.classify(), "channel");
247        assert_eq!(CamelError::RouteError("x".to_string()).classify(), "route");
248        assert_eq!(CamelError::Io("x".to_string()).classify(), "io");
249        assert_eq!(
250            CamelError::DeadLetterChannelFailed("x".to_string()).classify(),
251            "dead_letter"
252        );
253        assert_eq!(
254            CamelError::CircuitOpen("x".to_string()).classify(),
255            "circuit_open"
256        );
257        assert_eq!(
258            CamelError::HttpOperationFailed {
259                method: "GET".to_string(),
260                url: "https://example.com".to_string(),
261                status_code: 500,
262                status_text: "Internal Server Error".to_string(),
263                response_body: None,
264            }
265            .classify(),
266            "http"
267        );
268        assert_eq!(CamelError::Stopped.classify(), "stopped");
269        assert_eq!(CamelError::Config("x".to_string()).classify(), "config");
270        assert_eq!(CamelError::AlreadyConsumed.classify(), "type_conversion");
271        assert_eq!(CamelError::StreamLimitExceeded(42).classify(), "stream");
272    }
273
274    #[test]
275    fn test_classify_output_is_ascii_and_short() {
276        for error in all_error_samples() {
277            let class = error.classify();
278            assert!(class.is_ascii());
279            assert!(class.len() <= 15, "class too long: {class}");
280        }
281    }
282
283    #[test]
284    fn test_auth_variants_classify() {
285        assert_eq!(
286            CamelError::Unauthenticated("x".to_string()).classify(),
287            "unauthenticated"
288        );
289        assert_eq!(
290            CamelError::Unauthorized("x".to_string()).classify(),
291            "unauthorized"
292        );
293    }
294
295    #[test]
296    fn test_auth_variants_are_clone() {
297        let err = CamelError::Unauthenticated("test".to_string());
298        let cloned = err.clone();
299        assert!(matches!(cloned, CamelError::Unauthenticated(_)));
300
301        let err2 = CamelError::Unauthorized("test".to_string());
302        let cloned2 = err2.clone();
303        assert!(matches!(cloned2, CamelError::Unauthorized(_)));
304    }
305}
306
307#[cfg(test)]
308mod variant_name_tests {
309    use super::CamelError;
310    use std::sync::Arc;
311
312    /// Representative value for each of the 18 enum variants. This test fails to compile
313    /// when a new variant is added to CamelError without updating variant_name().
314    /// The enum is `#[non_exhaustive]` but this match lives in the same crate, so internal
315    /// exhaustive matching is allowed.
316    #[test]
317    fn variant_name_covers_all_variants() {
318        let cases: Vec<(CamelError, &str)> = vec![
319            (
320                CamelError::ComponentNotFound("x".into()),
321                "ComponentNotFound",
322            ),
323            (
324                CamelError::EndpointCreationFailed("x".into()),
325                "EndpointCreationFailed",
326            ),
327            (CamelError::ProcessorError("x".into()), "ProcessorError"),
328            (
329                CamelError::ProcessorErrorWithSource(
330                    "x".into(),
331                    Arc::new(std::io::Error::new(std::io::ErrorKind::Other, "y")),
332                ),
333                "ProcessorError", // aliased
334            ),
335            (
336                CamelError::TypeConversionFailed("x".into()),
337                "TypeConversionFailed",
338            ),
339            (CamelError::InvalidUri("x".into()), "InvalidUri"),
340            (CamelError::ChannelClosed, "ChannelClosed"),
341            (CamelError::RouteError("x".into()), "RouteError"),
342            (CamelError::Io("x".into()), "Io"),
343            (
344                CamelError::DeadLetterChannelFailed("x".into()),
345                "DeadLetterChannelFailed",
346            ),
347            (CamelError::CircuitOpen("x".into()), "CircuitOpen"),
348            (
349                CamelError::HttpOperationFailed {
350                    method: "GET".into(),
351                    url: "https://example.com".into(),
352                    status_code: 500,
353                    status_text: "Internal Server Error".into(),
354                    response_body: None,
355                },
356                "HttpOperationFailed",
357            ),
358            (CamelError::Stopped, "Stopped"),
359            (CamelError::Config("x".into()), "Config"),
360            (CamelError::AlreadyConsumed, "AlreadyConsumed"),
361            (CamelError::StreamLimitExceeded(42), "StreamLimitExceeded"),
362            (CamelError::Unauthenticated("x".into()), "Unauthenticated"),
363            (CamelError::Unauthorized("x".into()), "Unauthorized"),
364        ];
365
366        for (err, expected) in cases {
367            assert_eq!(
368                err.variant_name(),
369                expected,
370                "variant_name mismatch for {:?}",
371                err
372            );
373        }
374    }
375}