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    /// Producer's `poll_ready` returned a shutdown signal — the consumer/semaphore
53    /// is closing and the producer cannot acquire a permit. Distinct from Stop EIP
54    /// (which is successful control flow). Used by JMS/OpenSearch producers. See ADR-0024.
55    #[error("Consumer stopping: semaphore closed during poll_ready")]
56    ConsumerStopping,
57
58    #[error("Configuration error: {0}")]
59    Config(String),
60
61    #[error("Body stream has already been consumed")]
62    AlreadyConsumed,
63
64    #[error("Stream size exceeded limit: {0}")]
65    StreamLimitExceeded(usize),
66
67    #[error("Unauthenticated: {0}")]
68    Unauthenticated(String),
69
70    #[error("Unauthorized: {0}")]
71    Unauthorized(String),
72
73    #[error("Validation failed: {0}")]
74    ValidationError(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::ConsumerStopping => "consumer_stop",
92            Self::StreamLimitExceeded(_) => "stream",
93            Self::ChannelClosed => "channel",
94            Self::Unauthenticated(_) => "unauthenticated",
95            Self::Unauthorized(_) => "unauthorized",
96            Self::ValidationError(_) => "validation",
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::ConsumerStopping => "ConsumerStopping",
124            Self::Config(_) => "Config",
125            Self::AlreadyConsumed => "AlreadyConsumed",
126            Self::StreamLimitExceeded(_) => "StreamLimitExceeded",
127            Self::Unauthenticated(_) => "Unauthenticated",
128            Self::Unauthorized(_) => "Unauthorized",
129            Self::ValidationError(_) => "ValidationError",
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::ConsumerStopping,
174            CamelError::Config("x".to_string()),
175            CamelError::AlreadyConsumed,
176            CamelError::StreamLimitExceeded(42),
177            CamelError::Unauthenticated("token expired".to_string()),
178            CamelError::Unauthorized("missing admin role".to_string()),
179            CamelError::ValidationError("body does not match schema".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_classify_maps_all_variants() {
218        assert_eq!(
219            CamelError::ComponentNotFound("x".to_string()).classify(),
220            "component"
221        );
222        assert_eq!(
223            CamelError::EndpointCreationFailed("x".to_string()).classify(),
224            "endpoint"
225        );
226        assert_eq!(
227            CamelError::ProcessorError("x".to_string()).classify(),
228            "processor"
229        );
230        assert_eq!(
231            CamelError::TypeConversionFailed("x".to_string()).classify(),
232            "type_conversion"
233        );
234        assert_eq!(
235            CamelError::InvalidUri("x".to_string()).classify(),
236            "endpoint"
237        );
238        assert_eq!(CamelError::ChannelClosed.classify(), "channel");
239        assert_eq!(CamelError::RouteError("x".to_string()).classify(), "route");
240        assert_eq!(CamelError::Io("x".to_string()).classify(), "io");
241        assert_eq!(
242            CamelError::DeadLetterChannelFailed("x".to_string()).classify(),
243            "dead_letter"
244        );
245        assert_eq!(
246            CamelError::CircuitOpen("x".to_string()).classify(),
247            "circuit_open"
248        );
249        assert_eq!(
250            CamelError::HttpOperationFailed {
251                method: "GET".to_string(),
252                url: "https://example.com".to_string(),
253                status_code: 500,
254                status_text: "Internal Server Error".to_string(),
255                response_body: None,
256            }
257            .classify(),
258            "http"
259        );
260        assert_eq!(CamelError::Config("x".to_string()).classify(), "config");
261        assert_eq!(CamelError::AlreadyConsumed.classify(), "type_conversion");
262        assert_eq!(CamelError::StreamLimitExceeded(42).classify(), "stream");
263        assert_eq!(
264            CamelError::ValidationError("bad".to_string()).classify(),
265            "validation"
266        );
267    }
268
269    #[test]
270    fn test_classify_output_is_ascii_and_short() {
271        for error in all_error_samples() {
272            let class = error.classify();
273            assert!(class.is_ascii());
274            assert!(class.len() <= 15, "class too long: {class}");
275        }
276    }
277
278    #[test]
279    fn test_auth_variants_classify() {
280        assert_eq!(
281            CamelError::Unauthenticated("x".to_string()).classify(),
282            "unauthenticated"
283        );
284        assert_eq!(
285            CamelError::Unauthorized("x".to_string()).classify(),
286            "unauthorized"
287        );
288    }
289
290    #[test]
291    fn test_validation_error_classify() {
292        assert_eq!(
293            CamelError::ValidationError("bad".to_string()).classify(),
294            "validation"
295        );
296    }
297
298    #[test]
299    fn test_auth_variants_are_clone() {
300        let err = CamelError::Unauthenticated("test".to_string());
301        let cloned = err.clone();
302        assert!(matches!(cloned, CamelError::Unauthenticated(_)));
303
304        let err2 = CamelError::Unauthorized("test".to_string());
305        let cloned2 = err2.clone();
306        assert!(matches!(cloned2, CamelError::Unauthorized(_)));
307    }
308}
309
310#[cfg(test)]
311mod variant_name_tests {
312    use super::CamelError;
313    use std::sync::Arc;
314
315    /// Representative value for each enum variant. This test fails to compile
316    /// when a new variant is added to CamelError without updating variant_name().
317    /// The enum is `#[non_exhaustive]` but this match lives in the same crate, so internal
318    /// exhaustive matching is allowed.
319    #[test]
320    fn variant_name_covers_all_variants() {
321        let cases: Vec<(CamelError, &str)> = vec![
322            (
323                CamelError::ComponentNotFound("x".into()),
324                "ComponentNotFound",
325            ),
326            (
327                CamelError::EndpointCreationFailed("x".into()),
328                "EndpointCreationFailed",
329            ),
330            (CamelError::ProcessorError("x".into()), "ProcessorError"),
331            (
332                CamelError::ProcessorErrorWithSource(
333                    "x".into(),
334                    Arc::new(std::io::Error::new(std::io::ErrorKind::Other, "y")),
335                ),
336                "ProcessorError", // aliased
337            ),
338            (
339                CamelError::TypeConversionFailed("x".into()),
340                "TypeConversionFailed",
341            ),
342            (CamelError::InvalidUri("x".into()), "InvalidUri"),
343            (CamelError::ChannelClosed, "ChannelClosed"),
344            (CamelError::RouteError("x".into()), "RouteError"),
345            (CamelError::Io("x".into()), "Io"),
346            (
347                CamelError::DeadLetterChannelFailed("x".into()),
348                "DeadLetterChannelFailed",
349            ),
350            (CamelError::CircuitOpen("x".into()), "CircuitOpen"),
351            (
352                CamelError::HttpOperationFailed {
353                    method: "GET".into(),
354                    url: "https://example.com".into(),
355                    status_code: 500,
356                    status_text: "Internal Server Error".into(),
357                    response_body: None,
358                },
359                "HttpOperationFailed",
360            ),
361            (CamelError::Config("x".into()), "Config"),
362            (CamelError::AlreadyConsumed, "AlreadyConsumed"),
363            (CamelError::StreamLimitExceeded(42), "StreamLimitExceeded"),
364            (CamelError::Unauthenticated("x".into()), "Unauthenticated"),
365            (CamelError::Unauthorized("x".into()), "Unauthorized"),
366            (CamelError::ValidationError("bad".into()), "ValidationError"),
367        ];
368
369        for (err, expected) in cases {
370            assert_eq!(
371                err.variant_name(),
372                expected,
373                "variant_name mismatch for {:?}",
374                err
375            );
376        }
377    }
378}