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 #[error("Unsupported media type: consumed {consumed}, declared {declared}")]
171 UnsupportedMediaType { consumed: String, declared: String },
172
173 #[error("Not acceptable: accept {accept}, produced {produced}")]
176 NotAcceptable { accept: String, produced: String },
177}
178
179pub const CIRCUIT_OPEN: &str = "circuit_open";
185
186impl CamelError {
187 pub fn classify(&self) -> &'static str {
188 #[allow(unreachable_patterns)]
189 match self {
190 Self::ComponentNotFound(_) => "component",
191 Self::EndpointCreationFailed(_) | Self::InvalidUri(_) | Self::EndpointUri(_) => {
192 "endpoint"
193 }
194 Self::ProcessorError(_)
195 | Self::ProcessorErrorWithSource(_, _)
196 | Self::AuthProviderUnavailable(_) => "processor",
197 Self::TypeConversionFailed(_) | Self::AlreadyConsumed => "type_conversion",
198 Self::Io(_) => "io",
199 Self::RouteError(_) => "route",
200 Self::CircuitOpen(_) => CIRCUIT_OPEN,
201 Self::HttpOperationFailed { .. } => "http",
202 Self::Config(_) | Self::ConfigValidation(_) => "config",
203 Self::DeadLetterChannelFailed(_) => "dead_letter",
204 Self::ConsumerStopping => "consumer_stop",
205 Self::StreamLimitExceeded(_) => "stream",
206 Self::ChannelClosed => "channel",
207 Self::Unauthenticated(_) => "unauthenticated",
208 Self::Unauthorized(_) => "unauthorized",
209 Self::ValidationError(_) => "validation",
210 Self::TemplateReload(_) => "template",
211 Self::UnsupportedMediaType { .. } => "unsupported_media_type",
212 Self::NotAcceptable { .. } => "not_acceptable",
213 _ => "unknown",
214 }
215 }
216
217 pub fn variant_name(&self) -> &'static str {
227 match self {
228 Self::ComponentNotFound(_) => "ComponentNotFound",
229 Self::EndpointCreationFailed(_) => "EndpointCreationFailed",
230 Self::ProcessorError(_) => "ProcessorError",
231 Self::ProcessorErrorWithSource(_, _) => "ProcessorError",
232 Self::AuthProviderUnavailable(_) => "ProcessorError",
233 Self::TypeConversionFailed(_) => "TypeConversionFailed",
234 Self::InvalidUri(_) => "InvalidUri",
235 Self::ChannelClosed => "ChannelClosed",
236 Self::RouteError(_) => "RouteError",
237 Self::Io(_) => "Io",
238 Self::DeadLetterChannelFailed(_) => "DeadLetterChannelFailed",
239 Self::CircuitOpen(_) => "CircuitOpen",
240 Self::HttpOperationFailed { .. } => "HttpOperationFailed",
241 Self::ConsumerStopping => "ConsumerStopping",
242 Self::Config(_) => "Config",
243 Self::ConfigValidation(_) => "ConfigValidation",
244 Self::AlreadyConsumed => "AlreadyConsumed",
245 Self::StreamLimitExceeded(_) => "StreamLimitExceeded",
246 Self::Unauthenticated(_) => "Unauthenticated",
247 Self::Unauthorized(_) => "Unauthorized",
248 Self::ValidationError(_) => "ValidationError",
249 Self::TemplateReload(_) => "TemplateReload",
250 Self::EndpointUri(_) => "EndpointUri",
251 Self::UnsupportedMediaType { .. } => "UnsupportedMediaType",
252 Self::NotAcceptable { .. } => "NotAcceptable",
253 }
254 }
255}
256
257impl From<std::io::Error> for CamelError {
258 fn from(err: std::io::Error) -> Self {
259 CamelError::Io(err.to_string())
260 }
261}
262
263impl From<crate::template::TemplateError> for CamelError {
264 fn from(err: crate::template::TemplateError) -> Self {
265 CamelError::Config(err.to_string())
266 }
267}
268
269impl From<ConfigValidationError> for CamelError {
270 fn from(e: ConfigValidationError) -> Self {
271 CamelError::ConfigValidation(e)
272 }
273}
274
275impl From<EndpointUriError> for CamelError {
276 fn from(e: EndpointUriError) -> Self {
277 CamelError::EndpointUri(e)
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 fn all_error_samples() -> Vec<CamelError> {
286 vec![
287 CamelError::ComponentNotFound("x".to_string()),
288 CamelError::EndpointCreationFailed("x".to_string()),
289 CamelError::ProcessorError("x".to_string()),
290 CamelError::ProcessorErrorWithSource(
291 "x".to_string(),
292 Arc::new(std::io::Error::other("inner")),
293 ),
294 CamelError::TypeConversionFailed("x".to_string()),
295 CamelError::InvalidUri("x".to_string()),
296 CamelError::ChannelClosed,
297 CamelError::RouteError("x".to_string()),
298 CamelError::Io("x".to_string()),
299 CamelError::DeadLetterChannelFailed("x".to_string()),
300 CamelError::CircuitOpen("x".to_string()),
301 CamelError::HttpOperationFailed {
302 method: "GET".to_string(),
303 url: "https://example.com".to_string(),
304 status_code: 500,
305 status_text: "Internal Server Error".to_string(),
306 response_body: Some("error".to_string()),
307 },
308 CamelError::ConsumerStopping,
309 CamelError::Config("x".to_string()),
310 CamelError::ConfigValidation(ConfigValidationError::ThrottlerMaxRequestsZero),
311 CamelError::AlreadyConsumed,
312 CamelError::StreamLimitExceeded(42),
313 CamelError::Unauthenticated("token expired".to_string()),
314 CamelError::Unauthorized("missing admin role".to_string()),
315 CamelError::AuthProviderUnavailable("jwks down".to_string()),
316 CamelError::ValidationError("body does not match schema".to_string()),
317 CamelError::TemplateReload("reload failed".to_string()),
318 CamelError::EndpointUri(EndpointUriError::MissingScheme),
319 CamelError::UnsupportedMediaType {
320 consumed: "text/plain".to_string(),
321 declared: "application/json".to_string(),
322 },
323 CamelError::NotAcceptable {
324 accept: "application/xml".to_string(),
325 produced: "application/json".to_string(),
326 },
327 ]
328 }
329
330 #[test]
331 fn test_http_operation_failed_display() {
332 let err = CamelError::HttpOperationFailed {
333 method: "GET".to_string(),
334 url: "https://example.com/test".to_string(),
335 status_code: 404,
336 status_text: "Not Found".to_string(),
337 response_body: Some("page not found".to_string()),
338 };
339 let msg = format!("{err}");
340 assert!(msg.contains("404"));
341 assert!(msg.contains("Not Found"));
342 }
343
344 #[test]
345 fn test_http_operation_failed_clone() {
346 let err = CamelError::HttpOperationFailed {
347 method: "POST".to_string(),
348 url: "https://api.example.com/users".to_string(),
349 status_code: 500,
350 status_text: "Internal Server Error".to_string(),
351 response_body: None,
352 };
353 let cloned = err.clone();
354 assert!(matches!(
355 cloned,
356 CamelError::HttpOperationFailed {
357 status_code: 500,
358 ..
359 }
360 ));
361 }
362
363 #[test]
364 fn test_classify_maps_all_variants() {
365 assert_eq!(
366 CamelError::ComponentNotFound("x".to_string()).classify(),
367 "component"
368 );
369 assert_eq!(
370 CamelError::EndpointCreationFailed("x".to_string()).classify(),
371 "endpoint"
372 );
373 assert_eq!(
374 CamelError::ProcessorError("x".to_string()).classify(),
375 "processor"
376 );
377 assert_eq!(
378 CamelError::TypeConversionFailed("x".to_string()).classify(),
379 "type_conversion"
380 );
381 assert_eq!(
382 CamelError::InvalidUri("x".to_string()).classify(),
383 "endpoint"
384 );
385 assert_eq!(CamelError::ChannelClosed.classify(), "channel");
386 assert_eq!(CamelError::RouteError("x".to_string()).classify(), "route");
387 assert_eq!(CamelError::Io("x".to_string()).classify(), "io");
388 assert_eq!(
389 CamelError::DeadLetterChannelFailed("x".to_string()).classify(),
390 "dead_letter"
391 );
392 assert_eq!(
393 CamelError::CircuitOpen("x".to_string()).classify(),
394 "circuit_open"
395 );
396 assert_eq!(
397 CamelError::HttpOperationFailed {
398 method: "GET".to_string(),
399 url: "https://example.com".to_string(),
400 status_code: 500,
401 status_text: "Internal Server Error".to_string(),
402 response_body: None,
403 }
404 .classify(),
405 "http"
406 );
407 assert_eq!(CamelError::Config("x".to_string()).classify(), "config");
408 assert_eq!(
409 CamelError::ConfigValidation(ConfigValidationError::ThrottlerMaxRequestsZero)
410 .classify(),
411 "config"
412 );
413 assert_eq!(CamelError::AlreadyConsumed.classify(), "type_conversion");
414 assert_eq!(CamelError::StreamLimitExceeded(42).classify(), "stream");
415 assert_eq!(
416 CamelError::ValidationError("bad".to_string()).classify(),
417 "validation"
418 );
419 }
420
421 #[test]
422 fn test_classify_output_is_ascii_and_short() {
423 for error in all_error_samples() {
424 let class = error.classify();
425 assert!(class.is_ascii());
426 assert!(class.len() <= 22, "class too long: {class}");
428 }
429 }
430
431 #[test]
432 fn test_auth_variants_classify() {
433 assert_eq!(
434 CamelError::Unauthenticated("x".to_string()).classify(),
435 "unauthenticated"
436 );
437 assert_eq!(
438 CamelError::Unauthorized("x".to_string()).classify(),
439 "unauthorized"
440 );
441 }
442
443 #[test]
444 fn test_validation_error_classify() {
445 assert_eq!(
446 CamelError::ValidationError("bad".to_string()).classify(),
447 "validation"
448 );
449 }
450
451 #[test]
452 fn template_reload_classifies_as_template() {
453 let err = CamelError::TemplateReload("boom".into());
454 assert_eq!(err.classify(), "template");
455 }
456
457 #[test]
458 fn template_reload_variant_name() {
459 let err = CamelError::TemplateReload("boom".into());
460 assert_eq!(err.variant_name(), "TemplateReload");
461 }
462
463 #[test]
464 fn test_auth_variants_are_clone() {
465 let err = CamelError::Unauthenticated("test".to_string());
466 let cloned = err.clone();
467 assert!(matches!(cloned, CamelError::Unauthenticated(_)));
468
469 let err2 = CamelError::Unauthorized("test".to_string());
470 let cloned2 = err2.clone();
471 assert!(matches!(cloned2, CamelError::Unauthorized(_)));
472 }
473
474 #[test]
475 fn classification_unchanged_for_callers() {
476 assert_eq!(
481 CamelError::CircuitOpen("breaker open".into()).classify(),
482 "circuit_open"
483 );
484 }
485
486 #[test]
487 fn auth_provider_unavailable_display_carries_detail() {
488 let err = CamelError::AuthProviderUnavailable("conn refused".into());
489 let msg = err.to_string();
490 assert!(msg.contains("conn refused"));
491 assert!(
492 msg.starts_with("Auth provider unavailable"),
493 "display should start with 'Auth provider unavailable', got: {msg}"
494 );
495 }
496
497 #[test]
498 fn classify_negotiation_errors() {
499 let unsupported = CamelError::UnsupportedMediaType {
500 consumed: "text/plain".into(),
501 declared: "application/json".into(),
502 };
503 let not_acceptable = CamelError::NotAcceptable {
504 accept: "application/xml".into(),
505 produced: "application/json".into(),
506 };
507 assert_eq!(unsupported.classify(), "unsupported_media_type");
508 assert_eq!(not_acceptable.classify(), "not_acceptable");
509 }
510
511 #[test]
512 fn variant_names_negotiation_errors() {
513 let unsupported = CamelError::UnsupportedMediaType {
514 consumed: "text/plain".into(),
515 declared: "application/json".into(),
516 };
517 let not_acceptable = CamelError::NotAcceptable {
518 accept: "application/xml".into(),
519 produced: "application/json".into(),
520 };
521 assert_eq!(unsupported.variant_name(), "UnsupportedMediaType");
522 assert_eq!(not_acceptable.variant_name(), "NotAcceptable");
523 }
524
525 #[test]
526 fn display_negotiation_errors() {
527 let unsupported = CamelError::UnsupportedMediaType {
528 consumed: "text/plain".into(),
529 declared: "application/json".into(),
530 };
531 let not_acceptable = CamelError::NotAcceptable {
532 accept: "application/xml".into(),
533 produced: "application/json".into(),
534 };
535 let unsupported_msg = unsupported.to_string();
536 assert!(unsupported_msg.contains("text/plain"));
537 assert!(unsupported_msg.contains("application/json"));
538 let not_acceptable_msg = not_acceptable.to_string();
539 assert!(not_acceptable_msg.contains("application/xml"));
540 assert!(not_acceptable_msg.contains("application/json"));
541 }
542}
543
544#[cfg(test)]
545mod variant_name_tests {
546 use super::{CamelError, ConfigValidationError, EndpointUriError};
547 use std::sync::Arc;
548
549 #[test]
554 fn variant_name_covers_all_variants() {
555 let cases: Vec<(CamelError, &str)> = vec![
556 (
557 CamelError::ComponentNotFound("x".into()),
558 "ComponentNotFound",
559 ),
560 (
561 CamelError::EndpointCreationFailed("x".into()),
562 "EndpointCreationFailed",
563 ),
564 (CamelError::ProcessorError("x".into()), "ProcessorError"),
565 (
566 CamelError::ProcessorErrorWithSource(
567 "x".into(),
568 Arc::new(std::io::Error::other("y")),
569 ),
570 "ProcessorError", ),
572 (
573 CamelError::TypeConversionFailed("x".into()),
574 "TypeConversionFailed",
575 ),
576 (CamelError::InvalidUri("x".into()), "InvalidUri"),
577 (CamelError::ChannelClosed, "ChannelClosed"),
578 (CamelError::RouteError("x".into()), "RouteError"),
579 (CamelError::Io("x".into()), "Io"),
580 (
581 CamelError::DeadLetterChannelFailed("x".into()),
582 "DeadLetterChannelFailed",
583 ),
584 (CamelError::CircuitOpen("x".into()), "CircuitOpen"),
585 (
586 CamelError::HttpOperationFailed {
587 method: "GET".into(),
588 url: "https://example.com".into(),
589 status_code: 500,
590 status_text: "Internal Server Error".into(),
591 response_body: None,
592 },
593 "HttpOperationFailed",
594 ),
595 (CamelError::Config("x".into()), "Config"),
596 (
597 CamelError::ConfigValidation(ConfigValidationError::ThrottlerMaxRequestsZero),
598 "ConfigValidation",
599 ),
600 (CamelError::AlreadyConsumed, "AlreadyConsumed"),
601 (CamelError::StreamLimitExceeded(42), "StreamLimitExceeded"),
602 (CamelError::Unauthenticated("x".into()), "Unauthenticated"),
603 (CamelError::Unauthorized("x".into()), "Unauthorized"),
604 (CamelError::ValidationError("bad".into()), "ValidationError"),
605 (CamelError::TemplateReload("x".into()), "TemplateReload"),
606 (
607 CamelError::EndpointUri(EndpointUriError::MissingScheme),
608 "EndpointUri",
609 ),
610 (
611 CamelError::UnsupportedMediaType {
612 consumed: "text/plain".into(),
613 declared: "application/json".into(),
614 },
615 "UnsupportedMediaType",
616 ),
617 (
618 CamelError::NotAcceptable {
619 accept: "application/xml".into(),
620 produced: "application/json".into(),
621 },
622 "NotAcceptable",
623 ),
624 (
625 CamelError::AuthProviderUnavailable("x".into()),
626 "ProcessorError",
627 ),
628 ];
629
630 for (err, expected) in cases {
631 assert_eq!(
632 err.variant_name(),
633 expected,
634 "variant_name mismatch for {:?}",
635 err
636 );
637 }
638 }
639
640 #[test]
641 fn auth_provider_unavailable_classifies_as_processor() {
642 let err = CamelError::AuthProviderUnavailable("jwks down".into());
643 assert_eq!(err.classify(), "processor");
644 }
645
646 #[test]
647 fn auth_provider_unavailable_variant_name_aliases_processor_error() {
648 let err = CamelError::AuthProviderUnavailable("jwks down".into());
649 assert_eq!(err.variant_name(), "ProcessorError");
650 }
651}