1use std::fmt;
2use std::sync::Arc;
3use thiserror::Error;
4
5#[derive(Debug, Clone, PartialEq, Eq, Error)]
11#[non_exhaustive]
12pub enum ConfigValidationError {
13 #[error(
14 "aggregator config requires at least one completion bound (size, timeout, predicate, or interval)"
15 )]
16 AggregatorMissingCompletionBound,
17
18 #[error("aggregator requires at least one of max_buckets, completionTimeout, or bucket_ttl")]
23 AggregatorMissingMemoryBound,
24
25 #[error(
32 "aggregator Timeout completion requires bucket_ttl (memory-release bound for the timeout-task cap fallback)"
33 )]
34 AggregatorTimeoutRequiresTtl,
35
36 #[error("throttler max_requests must be > 0")]
37 ThrottlerMaxRequestsZero,
38
39 #[error("loop step must specify either 'count' or 'while', not both")]
40 LoopConflictingCountAndWhile,
41
42 #[error("on_exceptions clause cannot set both steps and handled_by (delegation is exclusive)")]
43 OnExceptionStepsHandledByConflict,
44
45 #[error("loop step must specify either 'count' or 'while'")]
46 LoopMissingCountOrWhile,
47
48 #[error("SQL use_message_body_for_sql requires allow_dynamic_query=true")]
49 SqlDynamicQueryWithoutAllowDynamic,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Error)]
58#[non_exhaustive]
59pub enum EndpointUriError {
60 #[error(
62 "endpoint URI parameter `{key}` duplicates a key already present in the base URI query"
63 )]
64 DuplicateKey { key: String },
65
66 #[error("endpoint URI is missing a scheme (expected `scheme:path`)")]
68 MissingScheme,
69
70 #[error("endpoint URI query contains a pair with an empty key")]
72 EmptyQueryKey,
73
74 #[error("endpoint URI parameter key `{key}` is empty or contains a reserved character")]
76 InvalidParamKey { key: String },
77}
78
79#[derive(Clone, Copy, Debug, PartialEq, Eq)]
86#[non_exhaustive]
87pub enum ExpressionErrorClass {
88 Runtime,
90 Arithmetic,
92 TypeMismatch,
94 FunctionNotFound,
96 Limit,
98 Timeout,
100 Conversion,
102 Parse,
104}
105
106impl ExpressionErrorClass {
107 pub fn as_str(&self) -> &'static str {
109 match self {
110 Self::Runtime => "runtime",
111 Self::Arithmetic => "arithmetic",
112 Self::TypeMismatch => "type-mismatch",
113 Self::FunctionNotFound => "function-not-found",
114 Self::Limit => "limit",
115 Self::Timeout => "timeout",
116 Self::Conversion => "conversion",
117 Self::Parse => "parse",
118 }
119 }
120}
121
122impl fmt::Display for ExpressionErrorClass {
123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124 f.write_str(self.as_str())
125 }
126}
127
128#[derive(Clone, Copy, Debug, PartialEq, Eq)]
131pub struct ErrorPosition {
132 pub line: u32,
134 pub column: u32,
136}
137
138impl fmt::Display for ErrorPosition {
139 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140 write!(f, "{}:{}", self.line, self.column)
141 }
142}
143
144#[derive(Clone, Debug, PartialEq, Eq)]
149pub struct ConversionDetail {
150 pub source_type: String,
152 pub target: String,
154}
155
156fn expression_position_suffix(position: &Option<ErrorPosition>) -> String {
158 match position {
159 Some(pos) => format!(" at {pos}"),
160 None => String::new(),
161 }
162}
163
164fn expression_conversion_suffix(conversion: &Option<ConversionDetail>) -> String {
167 match conversion {
168 Some(detail) => format!(
169 " while converting {} to {}",
170 detail.source_type, detail.target
171 ),
172 None => String::new(),
173 }
174}
175
176#[derive(Debug)]
228pub struct OpaqueErrorSource(Arc<dyn std::error::Error + Send + Sync>);
229
230impl OpaqueErrorSource {
231 pub fn new(source: Arc<dyn std::error::Error + Send + Sync>) -> Self {
233 Self(source)
234 }
235
236 fn clone_handle(&self) -> Self {
240 Self(Arc::clone(&self.0))
241 }
242}
243
244impl fmt::Display for OpaqueErrorSource {
245 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
246 self.0.fmt(f)
247 }
248}
249
250impl std::error::Error for OpaqueErrorSource {
251 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
252 Some(self.0.as_ref())
255 }
256}
257
258#[derive(Debug, Error)]
260#[non_exhaustive]
261pub enum CamelError {
262 #[error("Component not found: {0}")]
263 ComponentNotFound(String),
264
265 #[error("Endpoint creation failed: {0}")]
266 EndpointCreationFailed(String),
267
268 #[error("Endpoint creation failed: {0}")]
271 EndpointCreationFailedWithSource(String, #[source] OpaqueErrorSource),
272
273 #[error("Processor error: {0}")]
274 ProcessorError(String),
275
276 #[error("Processor error: {0}")]
279 ProcessorErrorWithSource(String, #[source] Arc<dyn std::error::Error + Send + Sync>),
280
281 #[error("Type conversion failed: {0}")]
282 TypeConversionFailed(String),
283
284 #[error("Invalid URI: {0}")]
285 InvalidUri(String),
286
287 #[error("Channel closed")]
288 ChannelClosed,
289
290 #[error("Route error: {0}")]
291 RouteError(String),
292
293 #[error("IO error: {0}")]
294 Io(String),
295
296 #[error("Dead letter channel failed: {0}")]
297 DeadLetterChannelFailed(String),
298
299 #[error("Circuit breaker open: {0}")]
300 CircuitOpen(String),
301
302 #[error("HTTP {method} {url} failed: {status_code} {status_text}")]
303 HttpOperationFailed {
304 method: String,
305 url: String,
306 status_code: u16,
307 status_text: String,
308 response_body: Option<String>,
309 },
310
311 #[error("Consumer stopping: semaphore closed during call")]
315 ConsumerStopping,
316
317 #[error("Configuration error: {0}")]
318 Config(String),
319
320 #[error("Configuration validation error: {0}")]
324 ConfigValidation(ConfigValidationError),
325
326 #[error("Body stream has already been consumed")]
327 AlreadyConsumed,
328
329 #[error("Stream size exceeded limit: {0}")]
330 StreamLimitExceeded(usize),
331
332 #[error("Unauthenticated: {0}")]
333 Unauthenticated(String),
334
335 #[error("Unauthorized: {0}")]
336 Unauthorized(String),
337
338 #[error("Auth provider unavailable: {0}")]
342 AuthProviderUnavailable(String),
343
344 #[error("Validation failed: {0}")]
345 ValidationError(String),
346
347 #[error("Template reload failed: {0}")]
348 TemplateReload(String),
349
350 #[error("Endpoint URI error: {0}")]
354 EndpointUri(EndpointUriError),
355
356 #[error("Unsupported media type: consumed {consumed}, declared {declared}")]
359 UnsupportedMediaType { consumed: String, declared: String },
360
361 #[error("Not acceptable: accept {accept}, produced {produced}")]
364 NotAcceptable { accept: String, produced: String },
365
366 #[error(
370 "expression failed: {class} in {language} `{verb}` step `{step_id}` (route `{route_id}`){}{}",
371 expression_position_suffix(.position),
372 expression_conversion_suffix(.conversion),
373 )]
374 ExpressionFailed {
375 language: String,
377 route_id: String,
379 step_id: String,
381 verb: String,
383 class: ExpressionErrorClass,
385 position: Option<ErrorPosition>,
387 conversion: Option<ConversionDetail>,
389 #[source]
393 cause: Option<Box<CamelError>>,
394 },
395}
396
397impl Clone for CamelError {
403 fn clone(&self) -> Self {
404 match self {
405 Self::ComponentNotFound(msg) => Self::ComponentNotFound(msg.clone()),
406 Self::EndpointCreationFailed(msg) => Self::EndpointCreationFailed(msg.clone()),
407 Self::EndpointCreationFailedWithSource(msg, source) => {
408 Self::EndpointCreationFailedWithSource(msg.clone(), source.clone_handle())
409 }
410 Self::ProcessorError(msg) => Self::ProcessorError(msg.clone()),
411 Self::ProcessorErrorWithSource(msg, source) => {
412 Self::ProcessorErrorWithSource(msg.clone(), Arc::clone(source))
413 }
414 Self::TypeConversionFailed(msg) => Self::TypeConversionFailed(msg.clone()),
415 Self::InvalidUri(msg) => Self::InvalidUri(msg.clone()),
416 Self::ChannelClosed => Self::ChannelClosed,
417 Self::RouteError(msg) => Self::RouteError(msg.clone()),
418 Self::Io(msg) => Self::Io(msg.clone()),
419 Self::DeadLetterChannelFailed(msg) => Self::DeadLetterChannelFailed(msg.clone()),
420 Self::CircuitOpen(msg) => Self::CircuitOpen(msg.clone()),
421 Self::HttpOperationFailed {
422 method,
423 url,
424 status_code,
425 status_text,
426 response_body,
427 } => Self::HttpOperationFailed {
428 method: method.clone(),
429 url: url.clone(),
430 status_code: *status_code,
431 status_text: status_text.clone(),
432 response_body: response_body.clone(),
433 },
434 Self::ConsumerStopping => Self::ConsumerStopping,
435 Self::Config(msg) => Self::Config(msg.clone()),
436 Self::ConfigValidation(e) => Self::ConfigValidation(e.clone()),
437 Self::AlreadyConsumed => Self::AlreadyConsumed,
438 Self::StreamLimitExceeded(limit) => Self::StreamLimitExceeded(*limit),
439 Self::Unauthenticated(msg) => Self::Unauthenticated(msg.clone()),
440 Self::Unauthorized(msg) => Self::Unauthorized(msg.clone()),
441 Self::AuthProviderUnavailable(msg) => Self::AuthProviderUnavailable(msg.clone()),
442 Self::ValidationError(msg) => Self::ValidationError(msg.clone()),
443 Self::TemplateReload(msg) => Self::TemplateReload(msg.clone()),
444 Self::EndpointUri(e) => Self::EndpointUri(e.clone()),
445 Self::UnsupportedMediaType { consumed, declared } => Self::UnsupportedMediaType {
446 consumed: consumed.clone(),
447 declared: declared.clone(),
448 },
449 Self::NotAcceptable { accept, produced } => Self::NotAcceptable {
450 accept: accept.clone(),
451 produced: produced.clone(),
452 },
453 Self::ExpressionFailed {
454 language,
455 route_id,
456 step_id,
457 verb,
458 class,
459 position,
460 conversion,
461 cause,
462 } => Self::ExpressionFailed {
463 language: language.clone(),
464 route_id: route_id.clone(),
465 step_id: step_id.clone(),
466 verb: verb.clone(),
467 class: *class,
468 position: *position,
469 conversion: conversion.clone(),
470 cause: cause.clone(),
471 },
472 }
473 }
474}
475
476pub const CIRCUIT_OPEN: &str = "circuit_open";
482
483impl CamelError {
484 pub fn classify(&self) -> &'static str {
485 #[allow(unreachable_patterns)]
486 match self {
487 Self::ComponentNotFound(_) => "component",
488 Self::EndpointCreationFailed(_)
489 | Self::EndpointCreationFailedWithSource(_, _)
490 | Self::InvalidUri(_)
491 | Self::EndpointUri(_) => "endpoint",
492 Self::ProcessorError(_)
493 | Self::ProcessorErrorWithSource(_, _)
494 | Self::AuthProviderUnavailable(_) => "processor",
495 Self::TypeConversionFailed(_) | Self::AlreadyConsumed => "type_conversion",
496 Self::Io(_) => "io",
497 Self::RouteError(_) => "route",
498 Self::CircuitOpen(_) => CIRCUIT_OPEN,
499 Self::HttpOperationFailed { .. } => "http",
500 Self::Config(_) | Self::ConfigValidation(_) => "config",
501 Self::DeadLetterChannelFailed(_) => "dead_letter",
502 Self::ConsumerStopping => "consumer_stop",
503 Self::StreamLimitExceeded(_) => "stream",
504 Self::ChannelClosed => "channel",
505 Self::Unauthenticated(_) => "unauthenticated",
506 Self::Unauthorized(_) => "unauthorized",
507 Self::ValidationError(_) => "validation",
508 Self::TemplateReload(_) => "template",
509 Self::UnsupportedMediaType { .. } => "unsupported_media_type",
510 Self::NotAcceptable { .. } => "not_acceptable",
511 _ => "unknown",
512 }
513 }
514
515 pub fn variant_name(&self) -> &'static str {
527 match self {
528 Self::ComponentNotFound(_) => "ComponentNotFound",
529 Self::EndpointCreationFailed(_) => "EndpointCreationFailed",
530 Self::EndpointCreationFailedWithSource(_, _) => "EndpointCreationFailed",
531 Self::ProcessorError(_) => "ProcessorError",
532 Self::ProcessorErrorWithSource(_, _) => "ProcessorError",
533 Self::AuthProviderUnavailable(_) => "ProcessorError",
534 Self::TypeConversionFailed(_) => "TypeConversionFailed",
535 Self::InvalidUri(_) => "InvalidUri",
536 Self::ChannelClosed => "ChannelClosed",
537 Self::RouteError(_) => "RouteError",
538 Self::Io(_) => "Io",
539 Self::DeadLetterChannelFailed(_) => "DeadLetterChannelFailed",
540 Self::CircuitOpen(_) => "CircuitOpen",
541 Self::HttpOperationFailed { .. } => "HttpOperationFailed",
542 Self::ConsumerStopping => "ConsumerStopping",
543 Self::Config(_) => "Config",
544 Self::ConfigValidation(_) => "ConfigValidation",
545 Self::AlreadyConsumed => "AlreadyConsumed",
546 Self::StreamLimitExceeded(_) => "StreamLimitExceeded",
547 Self::Unauthenticated(_) => "Unauthenticated",
548 Self::Unauthorized(_) => "Unauthorized",
549 Self::ValidationError(_) => "ValidationError",
550 Self::TemplateReload(_) => "TemplateReload",
551 Self::EndpointUri(_) => "EndpointUri",
552 Self::UnsupportedMediaType { .. } => "UnsupportedMediaType",
553 Self::NotAcceptable { .. } => "NotAcceptable",
554 Self::ExpressionFailed { .. } => "ExpressionFailed",
555 }
556 }
557}
558
559impl From<std::io::Error> for CamelError {
560 fn from(err: std::io::Error) -> Self {
561 CamelError::Io(err.to_string())
562 }
563}
564
565impl From<crate::template::TemplateError> for CamelError {
566 fn from(err: crate::template::TemplateError) -> Self {
567 CamelError::Config(err.to_string())
568 }
569}
570
571impl From<ConfigValidationError> for CamelError {
572 fn from(e: ConfigValidationError) -> Self {
573 CamelError::ConfigValidation(e)
574 }
575}
576
577impl From<EndpointUriError> for CamelError {
578 fn from(e: EndpointUriError) -> Self {
579 CamelError::EndpointUri(e)
580 }
581}
582
583pub type BoxError = std::boxed::Box<dyn std::error::Error + Send + Sync + 'static>;
585
586fn source_chain_contains_elapsed(err: &(dyn std::error::Error + 'static)) -> bool {
592 let mut current = Some(err);
593 while let Some(source) = current {
594 if source
595 .downcast_ref::<tower::timeout::error::Elapsed>()
596 .is_some()
597 {
598 return true;
599 }
600 current = std::error::Error::source(source);
601 }
602 false
603}
604
605impl From<BoxError> for CamelError {
681 fn from(err: BoxError) -> Self {
682 match err.downcast::<CamelError>() {
685 Ok(camel_error) => *camel_error,
686 Err(err) => {
687 let message = if source_chain_contains_elapsed(err.as_ref()) {
688 "middleware timeout elapsed"
689 } else {
690 "middleware error"
691 };
692 CamelError::ProcessorErrorWithSource(
693 message.to_string(),
694 Arc::new(OpaqueErrorSource::new(Arc::from(err))),
695 )
696 }
697 }
698 }
699}
700
701#[cfg(test)]
702mod tests {
703 use super::*;
704 use std::error::Error as _;
707
708 #[derive(Debug)]
710 struct SampleSource;
711
712 impl fmt::Display for SampleSource {
713 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
714 f.write_str("sample source")
715 }
716 }
717
718 impl std::error::Error for SampleSource {}
719
720 #[derive(Debug)]
725 struct ElapsedSourcedError(tower::timeout::error::Elapsed);
726
727 impl fmt::Display for ElapsedSourcedError {
728 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
729 f.write_str("wrapped middleware failure")
730 }
731 }
732
733 impl std::error::Error for ElapsedSourcedError {
734 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
735 Some(&self.0)
736 }
737 }
738
739 #[test]
740 fn boxed_camel_error_passthrough_unchanged() {
741 let boxed: Box<dyn std::error::Error + Send + Sync> =
742 Box::<dyn std::error::Error + Send + Sync>::from(CamelError::InvalidUri(
743 "u".to_string(),
744 ));
745 let err = CamelError::from(boxed);
746 match err {
747 CamelError::InvalidUri(uri) => assert_eq!(uri, "u"),
748 other => panic!("expected InvalidUri passthrough, got {other:?}"),
749 }
750 }
751
752 fn assert_elapsed_within_source_hops(err: &CamelError, max_hops: usize) {
760 let mut current = err.source();
761 for _ in 0..max_hops {
762 let Some(node) = current else { break };
763 if node
764 .downcast_ref::<tower::timeout::error::Elapsed>()
765 .is_some()
766 {
767 return;
768 }
769 current = node.source();
770 }
771 panic!("source chain does not reach tower Elapsed within {max_hops} hops: {err:?}");
772 }
773
774 #[test]
775 fn timeout_error_classified_through_source_chain() {
776 let boxed: Box<dyn std::error::Error + Send + Sync> =
777 Box::new(ElapsedSourcedError(tower::timeout::error::Elapsed::new()));
778 let err = CamelError::from(boxed);
779 assert!(
780 err.to_string().contains("middleware timeout elapsed"),
781 "unexpected display: {err}"
782 );
783 assert_elapsed_within_source_hops(&err, 3);
784 }
785
786 #[test]
787 fn bare_elapsed_root_error_classified_as_timeout() {
788 let boxed: Box<dyn std::error::Error + Send + Sync> =
792 Box::new(tower::timeout::error::Elapsed::new());
793 let err = CamelError::from(boxed);
794 assert!(
795 err.to_string().contains("middleware timeout elapsed"),
796 "unexpected display: {err}"
797 );
798 assert_elapsed_within_source_hops(&err, 3);
799 }
800
801 #[test]
802 #[allow(clippy::io_other_error)]
803 fn plain_box_error_preserves_source() {
804 let boxed: Box<dyn std::error::Error + Send + Sync> =
805 Box::new(std::io::Error::new(std::io::ErrorKind::Other, "boom"));
806 let err = CamelError::from(boxed);
807 match &err {
808 CamelError::ProcessorErrorWithSource(message, _) => {
809 assert!(
810 message.contains("middleware error"),
811 "unexpected message: {message}"
812 );
813 }
814 other => panic!("expected ProcessorErrorWithSource, got {other:?}"),
815 }
816 let source = err
817 .source()
818 .expect("ProcessorErrorWithSource carries a source");
819 assert_eq!(source.to_string(), "boom");
820 }
821
822 fn all_error_samples() -> Vec<CamelError> {
823 vec![
824 CamelError::ComponentNotFound("x".to_string()),
825 CamelError::EndpointCreationFailed("x".to_string()),
826 CamelError::EndpointCreationFailedWithSource(
827 "x".to_string(),
828 OpaqueErrorSource::new(Arc::new(SampleSource)),
829 ),
830 CamelError::ProcessorError("x".to_string()),
831 CamelError::ProcessorErrorWithSource(
832 "x".to_string(),
833 Arc::new(std::io::Error::other("inner")),
834 ),
835 CamelError::TypeConversionFailed("x".to_string()),
836 CamelError::InvalidUri("x".to_string()),
837 CamelError::ChannelClosed,
838 CamelError::RouteError("x".to_string()),
839 CamelError::Io("x".to_string()),
840 CamelError::DeadLetterChannelFailed("x".to_string()),
841 CamelError::CircuitOpen("x".to_string()),
842 CamelError::HttpOperationFailed {
843 method: "GET".to_string(),
844 url: "https://example.com".to_string(),
845 status_code: 500,
846 status_text: "Internal Server Error".to_string(),
847 response_body: Some("error".to_string()),
848 },
849 CamelError::ConsumerStopping,
850 CamelError::Config("x".to_string()),
851 CamelError::ConfigValidation(ConfigValidationError::ThrottlerMaxRequestsZero),
852 CamelError::AlreadyConsumed,
853 CamelError::StreamLimitExceeded(42),
854 CamelError::Unauthenticated("token expired".to_string()),
855 CamelError::Unauthorized("missing admin role".to_string()),
856 CamelError::AuthProviderUnavailable("jwks down".to_string()),
857 CamelError::ValidationError("body does not match schema".to_string()),
858 CamelError::TemplateReload("reload failed".to_string()),
859 CamelError::EndpointUri(EndpointUriError::MissingScheme),
860 CamelError::UnsupportedMediaType {
861 consumed: "text/plain".to_string(),
862 declared: "application/json".to_string(),
863 },
864 CamelError::NotAcceptable {
865 accept: "application/xml".to_string(),
866 produced: "application/json".to_string(),
867 },
868 ]
869 }
870
871 #[test]
872 fn test_http_operation_failed_display() {
873 let err = CamelError::HttpOperationFailed {
874 method: "GET".to_string(),
875 url: "https://example.com/test".to_string(),
876 status_code: 404,
877 status_text: "Not Found".to_string(),
878 response_body: Some("page not found".to_string()),
879 };
880 let msg = format!("{err}");
881 assert!(msg.contains("404"));
882 assert!(msg.contains("Not Found"));
883 }
884
885 #[test]
886 fn test_http_operation_failed_clone() {
887 let err = CamelError::HttpOperationFailed {
888 method: "POST".to_string(),
889 url: "https://api.example.com/users".to_string(),
890 status_code: 500,
891 status_text: "Internal Server Error".to_string(),
892 response_body: None,
893 };
894 let cloned = err.clone();
895 assert!(matches!(
896 cloned,
897 CamelError::HttpOperationFailed {
898 status_code: 500,
899 ..
900 }
901 ));
902 }
903
904 #[test]
905 fn test_classify_maps_all_variants() {
906 assert_eq!(
907 CamelError::ComponentNotFound("x".to_string()).classify(),
908 "component"
909 );
910 assert_eq!(
911 CamelError::EndpointCreationFailed("x".to_string()).classify(),
912 "endpoint"
913 );
914 assert_eq!(
915 CamelError::ProcessorError("x".to_string()).classify(),
916 "processor"
917 );
918 assert_eq!(
919 CamelError::TypeConversionFailed("x".to_string()).classify(),
920 "type_conversion"
921 );
922 assert_eq!(
923 CamelError::InvalidUri("x".to_string()).classify(),
924 "endpoint"
925 );
926 assert_eq!(CamelError::ChannelClosed.classify(), "channel");
927 assert_eq!(CamelError::RouteError("x".to_string()).classify(), "route");
928 assert_eq!(CamelError::Io("x".to_string()).classify(), "io");
929 assert_eq!(
930 CamelError::DeadLetterChannelFailed("x".to_string()).classify(),
931 "dead_letter"
932 );
933 assert_eq!(
934 CamelError::CircuitOpen("x".to_string()).classify(),
935 "circuit_open"
936 );
937 assert_eq!(
938 CamelError::HttpOperationFailed {
939 method: "GET".to_string(),
940 url: "https://example.com".to_string(),
941 status_code: 500,
942 status_text: "Internal Server Error".to_string(),
943 response_body: None,
944 }
945 .classify(),
946 "http"
947 );
948 assert_eq!(CamelError::Config("x".to_string()).classify(), "config");
949 assert_eq!(
950 CamelError::ConfigValidation(ConfigValidationError::ThrottlerMaxRequestsZero)
951 .classify(),
952 "config"
953 );
954 assert_eq!(CamelError::AlreadyConsumed.classify(), "type_conversion");
955 assert_eq!(CamelError::StreamLimitExceeded(42).classify(), "stream");
956 assert_eq!(
957 CamelError::ValidationError("bad".to_string()).classify(),
958 "validation"
959 );
960 }
961
962 #[test]
963 fn test_classify_output_is_ascii_and_short() {
964 for error in all_error_samples() {
965 let class = error.classify();
966 assert!(class.is_ascii());
967 assert!(class.len() <= 22, "class too long: {class}");
969 }
970 }
971
972 #[test]
973 fn test_auth_variants_classify() {
974 assert_eq!(
975 CamelError::Unauthenticated("x".to_string()).classify(),
976 "unauthenticated"
977 );
978 assert_eq!(
979 CamelError::Unauthorized("x".to_string()).classify(),
980 "unauthorized"
981 );
982 }
983
984 #[test]
985 fn test_validation_error_classify() {
986 assert_eq!(
987 CamelError::ValidationError("bad".to_string()).classify(),
988 "validation"
989 );
990 }
991
992 #[test]
993 fn template_reload_classifies_as_template() {
994 let err = CamelError::TemplateReload("boom".into());
995 assert_eq!(err.classify(), "template");
996 }
997
998 #[test]
999 fn template_reload_variant_name() {
1000 let err = CamelError::TemplateReload("boom".into());
1001 assert_eq!(err.variant_name(), "TemplateReload");
1002 }
1003
1004 #[test]
1005 fn test_auth_variants_are_clone() {
1006 let err = CamelError::Unauthenticated("test".to_string());
1007 let cloned = err.clone();
1008 assert!(matches!(cloned, CamelError::Unauthenticated(_)));
1009
1010 let err2 = CamelError::Unauthorized("test".to_string());
1011 let cloned2 = err2.clone();
1012 assert!(matches!(cloned2, CamelError::Unauthorized(_)));
1013 }
1014
1015 #[test]
1016 fn classification_unchanged_for_callers() {
1017 assert_eq!(
1022 CamelError::CircuitOpen("breaker open".into()).classify(),
1023 "circuit_open"
1024 );
1025 }
1026
1027 #[test]
1028 fn auth_provider_unavailable_display_carries_detail() {
1029 let err = CamelError::AuthProviderUnavailable("conn refused".into());
1030 let msg = err.to_string();
1031 assert!(msg.contains("conn refused"));
1032 assert!(
1033 msg.starts_with("Auth provider unavailable"),
1034 "display should start with 'Auth provider unavailable', got: {msg}"
1035 );
1036 }
1037
1038 #[test]
1039 fn classify_negotiation_errors() {
1040 let unsupported = CamelError::UnsupportedMediaType {
1041 consumed: "text/plain".into(),
1042 declared: "application/json".into(),
1043 };
1044 let not_acceptable = CamelError::NotAcceptable {
1045 accept: "application/xml".into(),
1046 produced: "application/json".into(),
1047 };
1048 assert_eq!(unsupported.classify(), "unsupported_media_type");
1049 assert_eq!(not_acceptable.classify(), "not_acceptable");
1050 }
1051
1052 #[test]
1053 fn variant_names_negotiation_errors() {
1054 let unsupported = CamelError::UnsupportedMediaType {
1055 consumed: "text/plain".into(),
1056 declared: "application/json".into(),
1057 };
1058 let not_acceptable = CamelError::NotAcceptable {
1059 accept: "application/xml".into(),
1060 produced: "application/json".into(),
1061 };
1062 assert_eq!(unsupported.variant_name(), "UnsupportedMediaType");
1063 assert_eq!(not_acceptable.variant_name(), "NotAcceptable");
1064 }
1065
1066 #[test]
1067 fn error_classification_label_is_pinned() {
1068 let type_conversion = CamelError::TypeConversionFailed(String::new());
1071 let validation = CamelError::ValidationError(String::new());
1072 let unsupported = CamelError::UnsupportedMediaType {
1073 consumed: String::new(),
1074 declared: String::new(),
1075 };
1076 let not_acceptable = CamelError::NotAcceptable {
1077 accept: String::new(),
1078 produced: String::new(),
1079 };
1080 let unauthenticated = CamelError::Unauthenticated(String::new());
1081 let unauthorized = CamelError::Unauthorized(String::new());
1082 let processor_with_source = CamelError::ProcessorErrorWithSource(
1083 String::new(),
1084 Arc::new(std::io::Error::other("inner")),
1085 );
1086 let consumer_stopping = CamelError::ConsumerStopping;
1087
1088 assert_eq!(
1089 type_conversion.classify(),
1090 "type_conversion",
1091 "TypeConversionFailed must classify as type_conversion (ADR-0096)"
1092 );
1093 assert_eq!(
1094 validation.classify(),
1095 "validation",
1096 "ValidationError must classify as validation"
1097 );
1098 assert_eq!(
1099 unsupported.classify(),
1100 "unsupported_media_type",
1101 "UnsupportedMediaType must classify as unsupported_media_type"
1102 );
1103 assert_eq!(
1104 not_acceptable.classify(),
1105 "not_acceptable",
1106 "NotAcceptable must classify as not_acceptable"
1107 );
1108 assert_eq!(
1109 unauthenticated.classify(),
1110 "unauthenticated",
1111 "Unauthenticated must classify as unauthenticated"
1112 );
1113 assert_eq!(
1114 unauthorized.classify(),
1115 "unauthorized",
1116 "Unauthorized must classify as unauthorized"
1117 );
1118 assert_eq!(
1119 processor_with_source.classify(),
1120 "processor",
1121 "ProcessorErrorWithSource must classify as processor"
1122 );
1123 assert_eq!(
1124 consumer_stopping.classify(),
1125 "consumer_stop",
1126 "ConsumerStopping must classify as consumer_stop"
1127 );
1128 }
1129
1130 #[test]
1131 fn display_negotiation_errors() {
1132 let unsupported = CamelError::UnsupportedMediaType {
1133 consumed: "text/plain".into(),
1134 declared: "application/json".into(),
1135 };
1136 let not_acceptable = CamelError::NotAcceptable {
1137 accept: "application/xml".into(),
1138 produced: "application/json".into(),
1139 };
1140 let unsupported_msg = unsupported.to_string();
1141 assert!(unsupported_msg.contains("text/plain"));
1142 assert!(unsupported_msg.contains("application/json"));
1143 let not_acceptable_msg = not_acceptable.to_string();
1144 assert!(not_acceptable_msg.contains("application/xml"));
1145 assert!(not_acceptable_msg.contains("application/json"));
1146 }
1147
1148 #[test]
1149 fn expression_failed_display_has_no_exchange_data() {
1150 let err = CamelError::ExpressionFailed {
1151 language: "rhai".to_string(),
1152 route_id: "r1".to_string(),
1153 step_id: "set_property#0".to_string(),
1154 verb: "set_property".to_string(),
1155 class: ExpressionErrorClass::Arithmetic,
1156 position: Some(ErrorPosition { line: 3, column: 8 }),
1157 conversion: None,
1158 cause: None,
1159 };
1160 let msg = err.to_string();
1161 assert!(msg.contains("arithmetic"), "missing class: {msg}");
1162 assert!(msg.contains("rhai"), "missing language: {msg}");
1163 assert!(msg.contains("set_property"), "missing verb/step: {msg}");
1164 assert!(msg.contains("3:8"), "missing position: {msg}");
1165 assert_eq!(
1167 msg,
1168 "expression failed: arithmetic in rhai `set_property` step \
1169 `set_property#0` (route `r1`) at 3:8"
1170 );
1171 }
1172
1173 #[test]
1174 fn expression_failed_variant_name() {
1175 let err = CamelError::ExpressionFailed {
1176 language: "rhai".to_string(),
1177 route_id: "r1".to_string(),
1178 step_id: "set_property#0".to_string(),
1179 verb: "set_property".to_string(),
1180 class: ExpressionErrorClass::Runtime,
1181 position: None,
1182 conversion: None,
1183 cause: None,
1184 };
1185 assert_eq!(err.variant_name(), "ExpressionFailed");
1186 }
1187
1188 #[test]
1189 fn config_validation_error_on_exception_conflict_display() {
1190 let err = ConfigValidationError::OnExceptionStepsHandledByConflict;
1191 let msg = format!("{err}");
1192 assert!(msg.contains("steps"));
1193 assert!(msg.contains("handled_by"));
1194 assert!(msg.contains("exclusive"));
1195 }
1196
1197 #[test]
1198 fn opaque_error_source_exposes_only_pointee() {
1199 let src = OpaqueErrorSource::new(Arc::new(SampleSource));
1200 let pointee = src.source().unwrap();
1201 assert!(pointee.downcast_ref::<SampleSource>().is_some());
1202 }
1203
1204 #[test]
1205 fn endpoint_creation_failed_with_source_aliases_to_plain() {
1206 let e = CamelError::EndpointCreationFailedWithSource(
1207 "d".to_string(),
1208 OpaqueErrorSource::new(Arc::new(SampleSource)),
1209 );
1210 assert_eq!(e.variant_name(), "EndpointCreationFailed");
1211 assert_eq!(e.classify(), "endpoint");
1212 assert_eq!(e.to_string(), "Endpoint creation failed: d");
1213 }
1214
1215 #[test]
1216 fn clone_preserves_variant_identity_for_all_error_samples() {
1217 for e in all_error_samples() {
1218 let c = e.clone();
1219 assert_eq!(c.variant_name(), e.variant_name());
1220 assert_eq!(c.classify(), e.classify());
1221 assert_eq!(c.to_string(), e.to_string());
1222 }
1223 }
1224
1225 #[test]
1226 fn camel_error_clone_preserves_source_provenance() {
1227 let e = CamelError::EndpointCreationFailedWithSource(
1228 "d".to_string(),
1229 OpaqueErrorSource::new(Arc::new(SampleSource)),
1230 );
1231 let c = e.clone();
1232 let wrapper = c.source().unwrap();
1236 let pointee = wrapper.source().unwrap();
1237 assert!(pointee.downcast_ref::<SampleSource>().is_some());
1238 }
1239}
1240
1241#[cfg(test)]
1242mod variant_name_tests {
1243 use super::{
1244 CamelError, ConfigValidationError, EndpointUriError, ExpressionErrorClass,
1245 OpaqueErrorSource,
1246 };
1247 use std::sync::Arc;
1248
1249 #[test]
1260 fn variant_name_covers_all_variants() {
1261 let cases: Vec<(CamelError, &str)> = vec![
1262 (
1263 CamelError::ComponentNotFound("x".into()),
1264 "ComponentNotFound",
1265 ),
1266 (
1267 CamelError::EndpointCreationFailed("x".into()),
1268 "EndpointCreationFailed",
1269 ),
1270 (
1271 CamelError::EndpointCreationFailedWithSource(
1272 "x".into(),
1273 OpaqueErrorSource::new(Arc::new(std::io::Error::other("y"))),
1274 ),
1275 "EndpointCreationFailed", ),
1277 (CamelError::ProcessorError("x".into()), "ProcessorError"),
1278 (
1279 CamelError::ProcessorErrorWithSource(
1280 "x".into(),
1281 Arc::new(std::io::Error::other("y")),
1282 ),
1283 "ProcessorError", ),
1285 (
1286 CamelError::TypeConversionFailed("x".into()),
1287 "TypeConversionFailed",
1288 ),
1289 (CamelError::InvalidUri("x".into()), "InvalidUri"),
1290 (CamelError::ChannelClosed, "ChannelClosed"),
1291 (CamelError::RouteError("x".into()), "RouteError"),
1292 (CamelError::Io("x".into()), "Io"),
1293 (
1294 CamelError::DeadLetterChannelFailed("x".into()),
1295 "DeadLetterChannelFailed",
1296 ),
1297 (CamelError::CircuitOpen("x".into()), "CircuitOpen"),
1298 (
1299 CamelError::HttpOperationFailed {
1300 method: "GET".into(),
1301 url: "https://example.com".into(),
1302 status_code: 500,
1303 status_text: "Internal Server Error".into(),
1304 response_body: None,
1305 },
1306 "HttpOperationFailed",
1307 ),
1308 (CamelError::ConsumerStopping, "ConsumerStopping"),
1309 (CamelError::Config("x".into()), "Config"),
1310 (
1311 CamelError::ConfigValidation(ConfigValidationError::ThrottlerMaxRequestsZero),
1312 "ConfigValidation",
1313 ),
1314 (CamelError::AlreadyConsumed, "AlreadyConsumed"),
1315 (CamelError::StreamLimitExceeded(42), "StreamLimitExceeded"),
1316 (CamelError::Unauthenticated("x".into()), "Unauthenticated"),
1317 (CamelError::Unauthorized("x".into()), "Unauthorized"),
1318 (CamelError::ValidationError("bad".into()), "ValidationError"),
1319 (CamelError::TemplateReload("x".into()), "TemplateReload"),
1320 (
1321 CamelError::EndpointUri(EndpointUriError::MissingScheme),
1322 "EndpointUri",
1323 ),
1324 (
1325 CamelError::UnsupportedMediaType {
1326 consumed: "text/plain".into(),
1327 declared: "application/json".into(),
1328 },
1329 "UnsupportedMediaType",
1330 ),
1331 (
1332 CamelError::NotAcceptable {
1333 accept: "application/xml".into(),
1334 produced: "application/json".into(),
1335 },
1336 "NotAcceptable",
1337 ),
1338 (
1339 CamelError::ExpressionFailed {
1340 language: "rhai".into(),
1341 route_id: "r1".into(),
1342 step_id: "set_property#0".into(),
1343 verb: "set_property".into(),
1344 class: ExpressionErrorClass::Runtime,
1345 position: None,
1346 conversion: None,
1347 cause: None,
1348 },
1349 "ExpressionFailed",
1350 ),
1351 (
1352 CamelError::AuthProviderUnavailable("x".into()),
1353 "ProcessorError",
1354 ),
1355 ];
1356
1357 assert_eq!(
1358 cases.len(),
1359 27,
1360 "variant_name_covers_all_variants must cover every CamelError variant; \
1361 extend this table and the camel-dsl classification guard"
1362 );
1363
1364 for (err, expected) in cases {
1365 assert_eq!(
1366 err.variant_name(),
1367 expected,
1368 "variant_name mismatch for {:?}",
1369 err
1370 );
1371 }
1372 }
1373
1374 #[test]
1375 fn auth_provider_unavailable_classifies_as_processor() {
1376 let err = CamelError::AuthProviderUnavailable("jwks down".into());
1377 assert_eq!(err.classify(), "processor");
1378 }
1379
1380 #[test]
1381 fn auth_provider_unavailable_variant_name_aliases_processor_error() {
1382 let err = CamelError::AuthProviderUnavailable("jwks down".into());
1383 assert_eq!(err.variant_name(), "ProcessorError");
1384 }
1385}