1use std::sync::Arc;
2use thiserror::Error;
3
4#[derive(Debug, Clone, PartialEq, Eq, Error)]
10#[non_exhaustive]
11pub enum ConfigValidationError {
12 #[error(
13 "aggregator config requires at least one completion bound (size, timeout, predicate, or interval)"
14 )]
15 AggregatorMissingCompletionBound,
16
17 #[error("aggregator requires at least one of max_buckets, completionTimeout, or bucket_ttl")]
22 AggregatorMissingMemoryBound,
23
24 #[error(
31 "aggregator Timeout completion requires bucket_ttl (memory-release bound for the timeout-task cap fallback)"
32 )]
33 AggregatorTimeoutRequiresTtl,
34
35 #[error("throttler max_requests must be > 0")]
36 ThrottlerMaxRequestsZero,
37
38 #[error("loop step must specify either 'count' or 'while', not both")]
39 LoopConflictingCountAndWhile,
40
41 #[error("loop step must specify either 'count' or 'while'")]
42 LoopMissingCountOrWhile,
43
44 #[error("SQL use_message_body_for_sql requires allow_dynamic_query=true")]
45 SqlDynamicQueryWithoutAllowDynamic,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Error)]
54#[non_exhaustive]
55pub enum EndpointUriError {
56 #[error(
58 "endpoint URI parameter `{key}` duplicates a key already present in the base URI query"
59 )]
60 DuplicateKey { key: String },
61
62 #[error("endpoint URI is missing a scheme (expected `scheme:path`)")]
64 MissingScheme,
65
66 #[error("endpoint URI query contains a pair with an empty key")]
68 EmptyQueryKey,
69
70 #[error("endpoint URI parameter key `{key}` is empty or contains a reserved character")]
72 InvalidParamKey { key: String },
73}
74
75#[derive(Debug, Clone, Error)]
77#[non_exhaustive]
78pub enum CamelError {
79 #[error("Component not found: {0}")]
80 ComponentNotFound(String),
81
82 #[error("Endpoint creation failed: {0}")]
83 EndpointCreationFailed(String),
84
85 #[error("Processor error: {0}")]
86 ProcessorError(String),
87
88 #[error("Processor error: {0}")]
91 ProcessorErrorWithSource(String, #[source] Arc<dyn std::error::Error + Send + Sync>),
92
93 #[error("Type conversion failed: {0}")]
94 TypeConversionFailed(String),
95
96 #[error("Invalid URI: {0}")]
97 InvalidUri(String),
98
99 #[error("Channel closed")]
100 ChannelClosed,
101
102 #[error("Route error: {0}")]
103 RouteError(String),
104
105 #[error("IO error: {0}")]
106 Io(String),
107
108 #[error("Dead letter channel failed: {0}")]
109 DeadLetterChannelFailed(String),
110
111 #[error("Circuit breaker open: {0}")]
112 CircuitOpen(String),
113
114 #[error("HTTP {method} {url} failed: {status_code} {status_text}")]
115 HttpOperationFailed {
116 method: String,
117 url: String,
118 status_code: u16,
119 status_text: String,
120 response_body: Option<String>,
121 },
122
123 #[error("Consumer stopping: semaphore closed during call")]
127 ConsumerStopping,
128
129 #[error("Configuration error: {0}")]
130 Config(String),
131
132 #[error("Configuration validation error: {0}")]
136 ConfigValidation(ConfigValidationError),
137
138 #[error("Body stream has already been consumed")]
139 AlreadyConsumed,
140
141 #[error("Stream size exceeded limit: {0}")]
142 StreamLimitExceeded(usize),
143
144 #[error("Unauthenticated: {0}")]
145 Unauthenticated(String),
146
147 #[error("Unauthorized: {0}")]
148 Unauthorized(String),
149
150 #[error("Auth provider unavailable: {0}")]
154 AuthProviderUnavailable(String),
155
156 #[error("Validation failed: {0}")]
157 ValidationError(String),
158
159 #[error("Template reload failed: {0}")]
160 TemplateReload(String),
161
162 #[error("Endpoint URI error: {0}")]
166 EndpointUri(EndpointUriError),
167}
168
169pub const CIRCUIT_OPEN: &str = "circuit_open";
175
176impl CamelError {
177 pub fn classify(&self) -> &'static str {
178 #[allow(unreachable_patterns)]
179 match self {
180 Self::ComponentNotFound(_) => "component",
181 Self::EndpointCreationFailed(_) | Self::InvalidUri(_) | Self::EndpointUri(_) => {
182 "endpoint"
183 }
184 Self::ProcessorError(_)
185 | Self::ProcessorErrorWithSource(_, _)
186 | Self::AuthProviderUnavailable(_) => "processor",
187 Self::TypeConversionFailed(_) | Self::AlreadyConsumed => "type_conversion",
188 Self::Io(_) => "io",
189 Self::RouteError(_) => "route",
190 Self::CircuitOpen(_) => CIRCUIT_OPEN,
191 Self::HttpOperationFailed { .. } => "http",
192 Self::Config(_) | Self::ConfigValidation(_) => "config",
193 Self::DeadLetterChannelFailed(_) => "dead_letter",
194 Self::ConsumerStopping => "consumer_stop",
195 Self::StreamLimitExceeded(_) => "stream",
196 Self::ChannelClosed => "channel",
197 Self::Unauthenticated(_) => "unauthenticated",
198 Self::Unauthorized(_) => "unauthorized",
199 Self::ValidationError(_) => "validation",
200 Self::TemplateReload(_) => "template",
201 _ => "unknown",
202 }
203 }
204
205 pub fn variant_name(&self) -> &'static str {
215 match self {
216 Self::ComponentNotFound(_) => "ComponentNotFound",
217 Self::EndpointCreationFailed(_) => "EndpointCreationFailed",
218 Self::ProcessorError(_) => "ProcessorError",
219 Self::ProcessorErrorWithSource(_, _) => "ProcessorError",
220 Self::AuthProviderUnavailable(_) => "ProcessorError",
221 Self::TypeConversionFailed(_) => "TypeConversionFailed",
222 Self::InvalidUri(_) => "InvalidUri",
223 Self::ChannelClosed => "ChannelClosed",
224 Self::RouteError(_) => "RouteError",
225 Self::Io(_) => "Io",
226 Self::DeadLetterChannelFailed(_) => "DeadLetterChannelFailed",
227 Self::CircuitOpen(_) => "CircuitOpen",
228 Self::HttpOperationFailed { .. } => "HttpOperationFailed",
229 Self::ConsumerStopping => "ConsumerStopping",
230 Self::Config(_) => "Config",
231 Self::ConfigValidation(_) => "ConfigValidation",
232 Self::AlreadyConsumed => "AlreadyConsumed",
233 Self::StreamLimitExceeded(_) => "StreamLimitExceeded",
234 Self::Unauthenticated(_) => "Unauthenticated",
235 Self::Unauthorized(_) => "Unauthorized",
236 Self::ValidationError(_) => "ValidationError",
237 Self::TemplateReload(_) => "TemplateReload",
238 Self::EndpointUri(_) => "EndpointUri",
239 }
240 }
241}
242
243impl From<std::io::Error> for CamelError {
244 fn from(err: std::io::Error) -> Self {
245 CamelError::Io(err.to_string())
246 }
247}
248
249impl From<crate::template::TemplateError> for CamelError {
250 fn from(err: crate::template::TemplateError) -> Self {
251 CamelError::Config(err.to_string())
252 }
253}
254
255impl From<ConfigValidationError> for CamelError {
256 fn from(e: ConfigValidationError) -> Self {
257 CamelError::ConfigValidation(e)
258 }
259}
260
261impl From<EndpointUriError> for CamelError {
262 fn from(e: EndpointUriError) -> Self {
263 CamelError::EndpointUri(e)
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 fn all_error_samples() -> Vec<CamelError> {
272 vec![
273 CamelError::ComponentNotFound("x".to_string()),
274 CamelError::EndpointCreationFailed("x".to_string()),
275 CamelError::ProcessorError("x".to_string()),
276 CamelError::ProcessorErrorWithSource(
277 "x".to_string(),
278 Arc::new(std::io::Error::other("inner")),
279 ),
280 CamelError::TypeConversionFailed("x".to_string()),
281 CamelError::InvalidUri("x".to_string()),
282 CamelError::ChannelClosed,
283 CamelError::RouteError("x".to_string()),
284 CamelError::Io("x".to_string()),
285 CamelError::DeadLetterChannelFailed("x".to_string()),
286 CamelError::CircuitOpen("x".to_string()),
287 CamelError::HttpOperationFailed {
288 method: "GET".to_string(),
289 url: "https://example.com".to_string(),
290 status_code: 500,
291 status_text: "Internal Server Error".to_string(),
292 response_body: Some("error".to_string()),
293 },
294 CamelError::ConsumerStopping,
295 CamelError::Config("x".to_string()),
296 CamelError::ConfigValidation(ConfigValidationError::ThrottlerMaxRequestsZero),
297 CamelError::AlreadyConsumed,
298 CamelError::StreamLimitExceeded(42),
299 CamelError::Unauthenticated("token expired".to_string()),
300 CamelError::Unauthorized("missing admin role".to_string()),
301 CamelError::AuthProviderUnavailable("jwks down".to_string()),
302 CamelError::ValidationError("body does not match schema".to_string()),
303 CamelError::TemplateReload("reload failed".to_string()),
304 CamelError::EndpointUri(EndpointUriError::MissingScheme),
305 ]
306 }
307
308 #[test]
309 fn test_http_operation_failed_display() {
310 let err = CamelError::HttpOperationFailed {
311 method: "GET".to_string(),
312 url: "https://example.com/test".to_string(),
313 status_code: 404,
314 status_text: "Not Found".to_string(),
315 response_body: Some("page not found".to_string()),
316 };
317 let msg = format!("{err}");
318 assert!(msg.contains("404"));
319 assert!(msg.contains("Not Found"));
320 }
321
322 #[test]
323 fn test_http_operation_failed_clone() {
324 let err = CamelError::HttpOperationFailed {
325 method: "POST".to_string(),
326 url: "https://api.example.com/users".to_string(),
327 status_code: 500,
328 status_text: "Internal Server Error".to_string(),
329 response_body: None,
330 };
331 let cloned = err.clone();
332 assert!(matches!(
333 cloned,
334 CamelError::HttpOperationFailed {
335 status_code: 500,
336 ..
337 }
338 ));
339 }
340
341 #[test]
342 fn test_classify_maps_all_variants() {
343 assert_eq!(
344 CamelError::ComponentNotFound("x".to_string()).classify(),
345 "component"
346 );
347 assert_eq!(
348 CamelError::EndpointCreationFailed("x".to_string()).classify(),
349 "endpoint"
350 );
351 assert_eq!(
352 CamelError::ProcessorError("x".to_string()).classify(),
353 "processor"
354 );
355 assert_eq!(
356 CamelError::TypeConversionFailed("x".to_string()).classify(),
357 "type_conversion"
358 );
359 assert_eq!(
360 CamelError::InvalidUri("x".to_string()).classify(),
361 "endpoint"
362 );
363 assert_eq!(CamelError::ChannelClosed.classify(), "channel");
364 assert_eq!(CamelError::RouteError("x".to_string()).classify(), "route");
365 assert_eq!(CamelError::Io("x".to_string()).classify(), "io");
366 assert_eq!(
367 CamelError::DeadLetterChannelFailed("x".to_string()).classify(),
368 "dead_letter"
369 );
370 assert_eq!(
371 CamelError::CircuitOpen("x".to_string()).classify(),
372 "circuit_open"
373 );
374 assert_eq!(
375 CamelError::HttpOperationFailed {
376 method: "GET".to_string(),
377 url: "https://example.com".to_string(),
378 status_code: 500,
379 status_text: "Internal Server Error".to_string(),
380 response_body: None,
381 }
382 .classify(),
383 "http"
384 );
385 assert_eq!(CamelError::Config("x".to_string()).classify(), "config");
386 assert_eq!(
387 CamelError::ConfigValidation(ConfigValidationError::ThrottlerMaxRequestsZero)
388 .classify(),
389 "config"
390 );
391 assert_eq!(CamelError::AlreadyConsumed.classify(), "type_conversion");
392 assert_eq!(CamelError::StreamLimitExceeded(42).classify(), "stream");
393 assert_eq!(
394 CamelError::ValidationError("bad".to_string()).classify(),
395 "validation"
396 );
397 }
398
399 #[test]
400 fn test_classify_output_is_ascii_and_short() {
401 for error in all_error_samples() {
402 let class = error.classify();
403 assert!(class.is_ascii());
404 assert!(class.len() <= 15, "class too long: {class}");
405 }
406 }
407
408 #[test]
409 fn test_auth_variants_classify() {
410 assert_eq!(
411 CamelError::Unauthenticated("x".to_string()).classify(),
412 "unauthenticated"
413 );
414 assert_eq!(
415 CamelError::Unauthorized("x".to_string()).classify(),
416 "unauthorized"
417 );
418 }
419
420 #[test]
421 fn test_validation_error_classify() {
422 assert_eq!(
423 CamelError::ValidationError("bad".to_string()).classify(),
424 "validation"
425 );
426 }
427
428 #[test]
429 fn template_reload_classifies_as_template() {
430 let err = CamelError::TemplateReload("boom".into());
431 assert_eq!(err.classify(), "template");
432 }
433
434 #[test]
435 fn template_reload_variant_name() {
436 let err = CamelError::TemplateReload("boom".into());
437 assert_eq!(err.variant_name(), "TemplateReload");
438 }
439
440 #[test]
441 fn test_auth_variants_are_clone() {
442 let err = CamelError::Unauthenticated("test".to_string());
443 let cloned = err.clone();
444 assert!(matches!(cloned, CamelError::Unauthenticated(_)));
445
446 let err2 = CamelError::Unauthorized("test".to_string());
447 let cloned2 = err2.clone();
448 assert!(matches!(cloned2, CamelError::Unauthorized(_)));
449 }
450
451 #[test]
452 fn classification_unchanged_for_callers() {
453 assert_eq!(
458 CamelError::CircuitOpen("breaker open".into()).classify(),
459 "circuit_open"
460 );
461 }
462
463 #[test]
464 fn auth_provider_unavailable_display_carries_detail() {
465 let err = CamelError::AuthProviderUnavailable("conn refused".into());
466 let msg = err.to_string();
467 assert!(msg.contains("conn refused"));
468 assert!(
469 msg.starts_with("Auth provider unavailable"),
470 "display should start with 'Auth provider unavailable', got: {msg}"
471 );
472 }
473}
474
475#[cfg(test)]
476mod variant_name_tests {
477 use super::{CamelError, ConfigValidationError, EndpointUriError};
478 use std::sync::Arc;
479
480 #[test]
485 fn variant_name_covers_all_variants() {
486 let cases: Vec<(CamelError, &str)> = vec![
487 (
488 CamelError::ComponentNotFound("x".into()),
489 "ComponentNotFound",
490 ),
491 (
492 CamelError::EndpointCreationFailed("x".into()),
493 "EndpointCreationFailed",
494 ),
495 (CamelError::ProcessorError("x".into()), "ProcessorError"),
496 (
497 CamelError::ProcessorErrorWithSource(
498 "x".into(),
499 Arc::new(std::io::Error::other("y")),
500 ),
501 "ProcessorError", ),
503 (
504 CamelError::TypeConversionFailed("x".into()),
505 "TypeConversionFailed",
506 ),
507 (CamelError::InvalidUri("x".into()), "InvalidUri"),
508 (CamelError::ChannelClosed, "ChannelClosed"),
509 (CamelError::RouteError("x".into()), "RouteError"),
510 (CamelError::Io("x".into()), "Io"),
511 (
512 CamelError::DeadLetterChannelFailed("x".into()),
513 "DeadLetterChannelFailed",
514 ),
515 (CamelError::CircuitOpen("x".into()), "CircuitOpen"),
516 (
517 CamelError::HttpOperationFailed {
518 method: "GET".into(),
519 url: "https://example.com".into(),
520 status_code: 500,
521 status_text: "Internal Server Error".into(),
522 response_body: None,
523 },
524 "HttpOperationFailed",
525 ),
526 (CamelError::Config("x".into()), "Config"),
527 (
528 CamelError::ConfigValidation(ConfigValidationError::ThrottlerMaxRequestsZero),
529 "ConfigValidation",
530 ),
531 (CamelError::AlreadyConsumed, "AlreadyConsumed"),
532 (CamelError::StreamLimitExceeded(42), "StreamLimitExceeded"),
533 (CamelError::Unauthenticated("x".into()), "Unauthenticated"),
534 (CamelError::Unauthorized("x".into()), "Unauthorized"),
535 (CamelError::ValidationError("bad".into()), "ValidationError"),
536 (CamelError::TemplateReload("x".into()), "TemplateReload"),
537 (
538 CamelError::EndpointUri(EndpointUriError::MissingScheme),
539 "EndpointUri",
540 ),
541 (
542 CamelError::AuthProviderUnavailable("x".into()),
543 "ProcessorError",
544 ),
545 ];
546
547 for (err, expected) in cases {
548 assert_eq!(
549 err.variant_name(),
550 expected,
551 "variant_name mismatch for {:?}",
552 err
553 );
554 }
555 }
556
557 #[test]
558 fn auth_provider_unavailable_classifies_as_processor() {
559 let err = CamelError::AuthProviderUnavailable("jwks down".into());
560 assert_eq!(err.classify(), "processor");
561 }
562
563 #[test]
564 fn auth_provider_unavailable_variant_name_aliases_processor_error() {
565 let err = CamelError::AuthProviderUnavailable("jwks down".into());
566 assert_eq!(err.variant_name(), "ProcessorError");
567 }
568}