use std::fmt;
use std::sync::Arc;
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[non_exhaustive]
pub enum ConfigValidationError {
#[error(
"aggregator config requires at least one completion bound (size, timeout, predicate, or interval)"
)]
AggregatorMissingCompletionBound,
#[error("aggregator requires at least one of max_buckets, completionTimeout, or bucket_ttl")]
AggregatorMissingMemoryBound,
#[error(
"aggregator Timeout completion requires bucket_ttl (memory-release bound for the timeout-task cap fallback)"
)]
AggregatorTimeoutRequiresTtl,
#[error("throttler max_requests must be > 0")]
ThrottlerMaxRequestsZero,
#[error("loop step must specify either 'count' or 'while', not both")]
LoopConflictingCountAndWhile,
#[error("loop step must specify either 'count' or 'while'")]
LoopMissingCountOrWhile,
#[error("SQL use_message_body_for_sql requires allow_dynamic_query=true")]
SqlDynamicQueryWithoutAllowDynamic,
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[non_exhaustive]
pub enum EndpointUriError {
#[error(
"endpoint URI parameter `{key}` duplicates a key already present in the base URI query"
)]
DuplicateKey { key: String },
#[error("endpoint URI is missing a scheme (expected `scheme:path`)")]
MissingScheme,
#[error("endpoint URI query contains a pair with an empty key")]
EmptyQueryKey,
#[error("endpoint URI parameter key `{key}` is empty or contains a reserved character")]
InvalidParamKey { key: String },
}
#[derive(Debug)]
pub struct OpaqueErrorSource(Arc<dyn std::error::Error + Send + Sync>);
impl OpaqueErrorSource {
pub fn new(source: Arc<dyn std::error::Error + Send + Sync>) -> Self {
Self(source)
}
fn clone_handle(&self) -> Self {
Self(Arc::clone(&self.0))
}
}
impl fmt::Display for OpaqueErrorSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl std::error::Error for OpaqueErrorSource {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(self.0.as_ref())
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum CamelError {
#[error("Component not found: {0}")]
ComponentNotFound(String),
#[error("Endpoint creation failed: {0}")]
EndpointCreationFailed(String),
#[error("Endpoint creation failed: {0}")]
EndpointCreationFailedWithSource(String, #[source] OpaqueErrorSource),
#[error("Processor error: {0}")]
ProcessorError(String),
#[error("Processor error: {0}")]
ProcessorErrorWithSource(String, #[source] Arc<dyn std::error::Error + Send + Sync>),
#[error("Type conversion failed: {0}")]
TypeConversionFailed(String),
#[error("Invalid URI: {0}")]
InvalidUri(String),
#[error("Channel closed")]
ChannelClosed,
#[error("Route error: {0}")]
RouteError(String),
#[error("IO error: {0}")]
Io(String),
#[error("Dead letter channel failed: {0}")]
DeadLetterChannelFailed(String),
#[error("Circuit breaker open: {0}")]
CircuitOpen(String),
#[error("HTTP {method} {url} failed: {status_code} {status_text}")]
HttpOperationFailed {
method: String,
url: String,
status_code: u16,
status_text: String,
response_body: Option<String>,
},
#[error("Consumer stopping: semaphore closed during call")]
ConsumerStopping,
#[error("Configuration error: {0}")]
Config(String),
#[error("Configuration validation error: {0}")]
ConfigValidation(ConfigValidationError),
#[error("Body stream has already been consumed")]
AlreadyConsumed,
#[error("Stream size exceeded limit: {0}")]
StreamLimitExceeded(usize),
#[error("Unauthenticated: {0}")]
Unauthenticated(String),
#[error("Unauthorized: {0}")]
Unauthorized(String),
#[error("Auth provider unavailable: {0}")]
AuthProviderUnavailable(String),
#[error("Validation failed: {0}")]
ValidationError(String),
#[error("Template reload failed: {0}")]
TemplateReload(String),
#[error("Endpoint URI error: {0}")]
EndpointUri(EndpointUriError),
#[error("Unsupported media type: consumed {consumed}, declared {declared}")]
UnsupportedMediaType { consumed: String, declared: String },
#[error("Not acceptable: accept {accept}, produced {produced}")]
NotAcceptable { accept: String, produced: String },
}
impl Clone for CamelError {
fn clone(&self) -> Self {
match self {
Self::ComponentNotFound(msg) => Self::ComponentNotFound(msg.clone()),
Self::EndpointCreationFailed(msg) => Self::EndpointCreationFailed(msg.clone()),
Self::EndpointCreationFailedWithSource(msg, source) => {
Self::EndpointCreationFailedWithSource(msg.clone(), source.clone_handle())
}
Self::ProcessorError(msg) => Self::ProcessorError(msg.clone()),
Self::ProcessorErrorWithSource(msg, source) => {
Self::ProcessorErrorWithSource(msg.clone(), Arc::clone(source))
}
Self::TypeConversionFailed(msg) => Self::TypeConversionFailed(msg.clone()),
Self::InvalidUri(msg) => Self::InvalidUri(msg.clone()),
Self::ChannelClosed => Self::ChannelClosed,
Self::RouteError(msg) => Self::RouteError(msg.clone()),
Self::Io(msg) => Self::Io(msg.clone()),
Self::DeadLetterChannelFailed(msg) => Self::DeadLetterChannelFailed(msg.clone()),
Self::CircuitOpen(msg) => Self::CircuitOpen(msg.clone()),
Self::HttpOperationFailed {
method,
url,
status_code,
status_text,
response_body,
} => Self::HttpOperationFailed {
method: method.clone(),
url: url.clone(),
status_code: *status_code,
status_text: status_text.clone(),
response_body: response_body.clone(),
},
Self::ConsumerStopping => Self::ConsumerStopping,
Self::Config(msg) => Self::Config(msg.clone()),
Self::ConfigValidation(e) => Self::ConfigValidation(e.clone()),
Self::AlreadyConsumed => Self::AlreadyConsumed,
Self::StreamLimitExceeded(limit) => Self::StreamLimitExceeded(*limit),
Self::Unauthenticated(msg) => Self::Unauthenticated(msg.clone()),
Self::Unauthorized(msg) => Self::Unauthorized(msg.clone()),
Self::AuthProviderUnavailable(msg) => Self::AuthProviderUnavailable(msg.clone()),
Self::ValidationError(msg) => Self::ValidationError(msg.clone()),
Self::TemplateReload(msg) => Self::TemplateReload(msg.clone()),
Self::EndpointUri(e) => Self::EndpointUri(e.clone()),
Self::UnsupportedMediaType { consumed, declared } => Self::UnsupportedMediaType {
consumed: consumed.clone(),
declared: declared.clone(),
},
Self::NotAcceptable { accept, produced } => Self::NotAcceptable {
accept: accept.clone(),
produced: produced.clone(),
},
}
}
}
pub const CIRCUIT_OPEN: &str = "circuit_open";
impl CamelError {
pub fn classify(&self) -> &'static str {
#[allow(unreachable_patterns)]
match self {
Self::ComponentNotFound(_) => "component",
Self::EndpointCreationFailed(_)
| Self::EndpointCreationFailedWithSource(_, _)
| Self::InvalidUri(_)
| Self::EndpointUri(_) => "endpoint",
Self::ProcessorError(_)
| Self::ProcessorErrorWithSource(_, _)
| Self::AuthProviderUnavailable(_) => "processor",
Self::TypeConversionFailed(_) | Self::AlreadyConsumed => "type_conversion",
Self::Io(_) => "io",
Self::RouteError(_) => "route",
Self::CircuitOpen(_) => CIRCUIT_OPEN,
Self::HttpOperationFailed { .. } => "http",
Self::Config(_) | Self::ConfigValidation(_) => "config",
Self::DeadLetterChannelFailed(_) => "dead_letter",
Self::ConsumerStopping => "consumer_stop",
Self::StreamLimitExceeded(_) => "stream",
Self::ChannelClosed => "channel",
Self::Unauthenticated(_) => "unauthenticated",
Self::Unauthorized(_) => "unauthorized",
Self::ValidationError(_) => "validation",
Self::TemplateReload(_) => "template",
Self::UnsupportedMediaType { .. } => "unsupported_media_type",
Self::NotAcceptable { .. } => "not_acceptable",
_ => "unknown",
}
}
pub fn variant_name(&self) -> &'static str {
match self {
Self::ComponentNotFound(_) => "ComponentNotFound",
Self::EndpointCreationFailed(_) => "EndpointCreationFailed",
Self::EndpointCreationFailedWithSource(_, _) => "EndpointCreationFailed",
Self::ProcessorError(_) => "ProcessorError",
Self::ProcessorErrorWithSource(_, _) => "ProcessorError",
Self::AuthProviderUnavailable(_) => "ProcessorError",
Self::TypeConversionFailed(_) => "TypeConversionFailed",
Self::InvalidUri(_) => "InvalidUri",
Self::ChannelClosed => "ChannelClosed",
Self::RouteError(_) => "RouteError",
Self::Io(_) => "Io",
Self::DeadLetterChannelFailed(_) => "DeadLetterChannelFailed",
Self::CircuitOpen(_) => "CircuitOpen",
Self::HttpOperationFailed { .. } => "HttpOperationFailed",
Self::ConsumerStopping => "ConsumerStopping",
Self::Config(_) => "Config",
Self::ConfigValidation(_) => "ConfigValidation",
Self::AlreadyConsumed => "AlreadyConsumed",
Self::StreamLimitExceeded(_) => "StreamLimitExceeded",
Self::Unauthenticated(_) => "Unauthenticated",
Self::Unauthorized(_) => "Unauthorized",
Self::ValidationError(_) => "ValidationError",
Self::TemplateReload(_) => "TemplateReload",
Self::EndpointUri(_) => "EndpointUri",
Self::UnsupportedMediaType { .. } => "UnsupportedMediaType",
Self::NotAcceptable { .. } => "NotAcceptable",
}
}
}
impl From<std::io::Error> for CamelError {
fn from(err: std::io::Error) -> Self {
CamelError::Io(err.to_string())
}
}
impl From<crate::template::TemplateError> for CamelError {
fn from(err: crate::template::TemplateError) -> Self {
CamelError::Config(err.to_string())
}
}
impl From<ConfigValidationError> for CamelError {
fn from(e: ConfigValidationError) -> Self {
CamelError::ConfigValidation(e)
}
}
impl From<EndpointUriError> for CamelError {
fn from(e: EndpointUriError) -> Self {
CamelError::EndpointUri(e)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error as _;
#[derive(Debug)]
struct SampleSource;
impl fmt::Display for SampleSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("sample source")
}
}
impl std::error::Error for SampleSource {}
fn all_error_samples() -> Vec<CamelError> {
vec![
CamelError::ComponentNotFound("x".to_string()),
CamelError::EndpointCreationFailed("x".to_string()),
CamelError::EndpointCreationFailedWithSource(
"x".to_string(),
OpaqueErrorSource::new(Arc::new(SampleSource)),
),
CamelError::ProcessorError("x".to_string()),
CamelError::ProcessorErrorWithSource(
"x".to_string(),
Arc::new(std::io::Error::other("inner")),
),
CamelError::TypeConversionFailed("x".to_string()),
CamelError::InvalidUri("x".to_string()),
CamelError::ChannelClosed,
CamelError::RouteError("x".to_string()),
CamelError::Io("x".to_string()),
CamelError::DeadLetterChannelFailed("x".to_string()),
CamelError::CircuitOpen("x".to_string()),
CamelError::HttpOperationFailed {
method: "GET".to_string(),
url: "https://example.com".to_string(),
status_code: 500,
status_text: "Internal Server Error".to_string(),
response_body: Some("error".to_string()),
},
CamelError::ConsumerStopping,
CamelError::Config("x".to_string()),
CamelError::ConfigValidation(ConfigValidationError::ThrottlerMaxRequestsZero),
CamelError::AlreadyConsumed,
CamelError::StreamLimitExceeded(42),
CamelError::Unauthenticated("token expired".to_string()),
CamelError::Unauthorized("missing admin role".to_string()),
CamelError::AuthProviderUnavailable("jwks down".to_string()),
CamelError::ValidationError("body does not match schema".to_string()),
CamelError::TemplateReload("reload failed".to_string()),
CamelError::EndpointUri(EndpointUriError::MissingScheme),
CamelError::UnsupportedMediaType {
consumed: "text/plain".to_string(),
declared: "application/json".to_string(),
},
CamelError::NotAcceptable {
accept: "application/xml".to_string(),
produced: "application/json".to_string(),
},
]
}
#[test]
fn test_http_operation_failed_display() {
let err = CamelError::HttpOperationFailed {
method: "GET".to_string(),
url: "https://example.com/test".to_string(),
status_code: 404,
status_text: "Not Found".to_string(),
response_body: Some("page not found".to_string()),
};
let msg = format!("{err}");
assert!(msg.contains("404"));
assert!(msg.contains("Not Found"));
}
#[test]
fn test_http_operation_failed_clone() {
let err = CamelError::HttpOperationFailed {
method: "POST".to_string(),
url: "https://api.example.com/users".to_string(),
status_code: 500,
status_text: "Internal Server Error".to_string(),
response_body: None,
};
let cloned = err.clone();
assert!(matches!(
cloned,
CamelError::HttpOperationFailed {
status_code: 500,
..
}
));
}
#[test]
fn test_classify_maps_all_variants() {
assert_eq!(
CamelError::ComponentNotFound("x".to_string()).classify(),
"component"
);
assert_eq!(
CamelError::EndpointCreationFailed("x".to_string()).classify(),
"endpoint"
);
assert_eq!(
CamelError::ProcessorError("x".to_string()).classify(),
"processor"
);
assert_eq!(
CamelError::TypeConversionFailed("x".to_string()).classify(),
"type_conversion"
);
assert_eq!(
CamelError::InvalidUri("x".to_string()).classify(),
"endpoint"
);
assert_eq!(CamelError::ChannelClosed.classify(), "channel");
assert_eq!(CamelError::RouteError("x".to_string()).classify(), "route");
assert_eq!(CamelError::Io("x".to_string()).classify(), "io");
assert_eq!(
CamelError::DeadLetterChannelFailed("x".to_string()).classify(),
"dead_letter"
);
assert_eq!(
CamelError::CircuitOpen("x".to_string()).classify(),
"circuit_open"
);
assert_eq!(
CamelError::HttpOperationFailed {
method: "GET".to_string(),
url: "https://example.com".to_string(),
status_code: 500,
status_text: "Internal Server Error".to_string(),
response_body: None,
}
.classify(),
"http"
);
assert_eq!(CamelError::Config("x".to_string()).classify(), "config");
assert_eq!(
CamelError::ConfigValidation(ConfigValidationError::ThrottlerMaxRequestsZero)
.classify(),
"config"
);
assert_eq!(CamelError::AlreadyConsumed.classify(), "type_conversion");
assert_eq!(CamelError::StreamLimitExceeded(42).classify(), "stream");
assert_eq!(
CamelError::ValidationError("bad".to_string()).classify(),
"validation"
);
}
#[test]
fn test_classify_output_is_ascii_and_short() {
for error in all_error_samples() {
let class = error.classify();
assert!(class.is_ascii());
assert!(class.len() <= 22, "class too long: {class}");
}
}
#[test]
fn test_auth_variants_classify() {
assert_eq!(
CamelError::Unauthenticated("x".to_string()).classify(),
"unauthenticated"
);
assert_eq!(
CamelError::Unauthorized("x".to_string()).classify(),
"unauthorized"
);
}
#[test]
fn test_validation_error_classify() {
assert_eq!(
CamelError::ValidationError("bad".to_string()).classify(),
"validation"
);
}
#[test]
fn template_reload_classifies_as_template() {
let err = CamelError::TemplateReload("boom".into());
assert_eq!(err.classify(), "template");
}
#[test]
fn template_reload_variant_name() {
let err = CamelError::TemplateReload("boom".into());
assert_eq!(err.variant_name(), "TemplateReload");
}
#[test]
fn test_auth_variants_are_clone() {
let err = CamelError::Unauthenticated("test".to_string());
let cloned = err.clone();
assert!(matches!(cloned, CamelError::Unauthenticated(_)));
let err2 = CamelError::Unauthorized("test".to_string());
let cloned2 = err2.clone();
assert!(matches!(cloned2, CamelError::Unauthorized(_)));
}
#[test]
fn classification_unchanged_for_callers() {
assert_eq!(
CamelError::CircuitOpen("breaker open".into()).classify(),
"circuit_open"
);
}
#[test]
fn auth_provider_unavailable_display_carries_detail() {
let err = CamelError::AuthProviderUnavailable("conn refused".into());
let msg = err.to_string();
assert!(msg.contains("conn refused"));
assert!(
msg.starts_with("Auth provider unavailable"),
"display should start with 'Auth provider unavailable', got: {msg}"
);
}
#[test]
fn classify_negotiation_errors() {
let unsupported = CamelError::UnsupportedMediaType {
consumed: "text/plain".into(),
declared: "application/json".into(),
};
let not_acceptable = CamelError::NotAcceptable {
accept: "application/xml".into(),
produced: "application/json".into(),
};
assert_eq!(unsupported.classify(), "unsupported_media_type");
assert_eq!(not_acceptable.classify(), "not_acceptable");
}
#[test]
fn variant_names_negotiation_errors() {
let unsupported = CamelError::UnsupportedMediaType {
consumed: "text/plain".into(),
declared: "application/json".into(),
};
let not_acceptable = CamelError::NotAcceptable {
accept: "application/xml".into(),
produced: "application/json".into(),
};
assert_eq!(unsupported.variant_name(), "UnsupportedMediaType");
assert_eq!(not_acceptable.variant_name(), "NotAcceptable");
}
#[test]
fn display_negotiation_errors() {
let unsupported = CamelError::UnsupportedMediaType {
consumed: "text/plain".into(),
declared: "application/json".into(),
};
let not_acceptable = CamelError::NotAcceptable {
accept: "application/xml".into(),
produced: "application/json".into(),
};
let unsupported_msg = unsupported.to_string();
assert!(unsupported_msg.contains("text/plain"));
assert!(unsupported_msg.contains("application/json"));
let not_acceptable_msg = not_acceptable.to_string();
assert!(not_acceptable_msg.contains("application/xml"));
assert!(not_acceptable_msg.contains("application/json"));
}
#[test]
fn opaque_error_source_exposes_only_pointee() {
let src = OpaqueErrorSource::new(Arc::new(SampleSource));
let pointee = src.source().unwrap();
assert!(pointee.downcast_ref::<SampleSource>().is_some());
}
#[test]
fn endpoint_creation_failed_with_source_aliases_to_plain() {
let e = CamelError::EndpointCreationFailedWithSource(
"d".to_string(),
OpaqueErrorSource::new(Arc::new(SampleSource)),
);
assert_eq!(e.variant_name(), "EndpointCreationFailed");
assert_eq!(e.classify(), "endpoint");
assert_eq!(e.to_string(), "Endpoint creation failed: d");
}
#[test]
fn clone_preserves_variant_identity_for_all_error_samples() {
for e in all_error_samples() {
let c = e.clone();
assert_eq!(c.variant_name(), e.variant_name());
assert_eq!(c.classify(), e.classify());
assert_eq!(c.to_string(), e.to_string());
}
}
#[test]
fn camel_error_clone_preserves_source_provenance() {
let e = CamelError::EndpointCreationFailedWithSource(
"d".to_string(),
OpaqueErrorSource::new(Arc::new(SampleSource)),
);
let c = e.clone();
let wrapper = c.source().unwrap();
let pointee = wrapper.source().unwrap();
assert!(pointee.downcast_ref::<SampleSource>().is_some());
}
}
#[cfg(test)]
mod variant_name_tests {
use super::{CamelError, ConfigValidationError, EndpointUriError, OpaqueErrorSource};
use std::sync::Arc;
#[test]
fn variant_name_covers_all_variants() {
let cases: Vec<(CamelError, &str)> = vec![
(
CamelError::ComponentNotFound("x".into()),
"ComponentNotFound",
),
(
CamelError::EndpointCreationFailed("x".into()),
"EndpointCreationFailed",
),
(
CamelError::EndpointCreationFailedWithSource(
"x".into(),
OpaqueErrorSource::new(Arc::new(std::io::Error::other("y"))),
),
"EndpointCreationFailed", ),
(CamelError::ProcessorError("x".into()), "ProcessorError"),
(
CamelError::ProcessorErrorWithSource(
"x".into(),
Arc::new(std::io::Error::other("y")),
),
"ProcessorError", ),
(
CamelError::TypeConversionFailed("x".into()),
"TypeConversionFailed",
),
(CamelError::InvalidUri("x".into()), "InvalidUri"),
(CamelError::ChannelClosed, "ChannelClosed"),
(CamelError::RouteError("x".into()), "RouteError"),
(CamelError::Io("x".into()), "Io"),
(
CamelError::DeadLetterChannelFailed("x".into()),
"DeadLetterChannelFailed",
),
(CamelError::CircuitOpen("x".into()), "CircuitOpen"),
(
CamelError::HttpOperationFailed {
method: "GET".into(),
url: "https://example.com".into(),
status_code: 500,
status_text: "Internal Server Error".into(),
response_body: None,
},
"HttpOperationFailed",
),
(CamelError::ConsumerStopping, "ConsumerStopping"),
(CamelError::Config("x".into()), "Config"),
(
CamelError::ConfigValidation(ConfigValidationError::ThrottlerMaxRequestsZero),
"ConfigValidation",
),
(CamelError::AlreadyConsumed, "AlreadyConsumed"),
(CamelError::StreamLimitExceeded(42), "StreamLimitExceeded"),
(CamelError::Unauthenticated("x".into()), "Unauthenticated"),
(CamelError::Unauthorized("x".into()), "Unauthorized"),
(CamelError::ValidationError("bad".into()), "ValidationError"),
(CamelError::TemplateReload("x".into()), "TemplateReload"),
(
CamelError::EndpointUri(EndpointUriError::MissingScheme),
"EndpointUri",
),
(
CamelError::UnsupportedMediaType {
consumed: "text/plain".into(),
declared: "application/json".into(),
},
"UnsupportedMediaType",
),
(
CamelError::NotAcceptable {
accept: "application/xml".into(),
produced: "application/json".into(),
},
"NotAcceptable",
),
(
CamelError::AuthProviderUnavailable("x".into()),
"ProcessorError",
),
];
assert_eq!(
cases.len(),
26,
"variant_name_covers_all_variants must cover every CamelError variant; \
extend this table and the camel-dsl classification guard"
);
for (err, expected) in cases {
assert_eq!(
err.variant_name(),
expected,
"variant_name mismatch for {:?}",
err
);
}
}
#[test]
fn auth_provider_unavailable_classifies_as_processor() {
let err = CamelError::AuthProviderUnavailable("jwks down".into());
assert_eq!(err.classify(), "processor");
}
#[test]
fn auth_provider_unavailable_variant_name_aliases_processor_error() {
let err = CamelError::AuthProviderUnavailable("jwks down".into());
assert_eq!(err.variant_name(), "ProcessorError");
}
}