Skip to main content

camel_processor/
error_handler.rs

1use std::future::Future;
2use std::pin::Pin;
3use std::task::{Context, Poll};
4
5use tower::{Layer, Service, ServiceExt};
6
7use camel_api::error_handler::{
8    BoundaryKind, ExceptionDisposition, ExceptionPolicy, HEADER_REDELIVERED,
9    HEADER_REDELIVERY_COUNTER, HEADER_REDELIVERY_MAX_COUNTER, PolicyId, RetryOutcome,
10    StepDisposition,
11};
12use camel_api::{BoxProcessor, CamelError, Exchange, PipelineOutcome, SyncBoxProcessor, Value};
13
14async fn execute_on_steps(
15    original: Exchange,
16    original_err: CamelError,
17    on_steps: &SyncBoxProcessor,
18    disposition: ExceptionDisposition,
19    handler: Option<BoxProcessor>,
20) -> Result<Exchange, CamelError> {
21    let snapshot = original.clone();
22    let mut ex = original;
23    ex.set_error(original_err.clone());
24    let mut pipeline = on_steps.clone_inner();
25    let step_result = async {
26        let svc = pipeline.ready().await?;
27        svc.call(ex).await
28    }
29    .await;
30
31    match step_result {
32        Ok(mut ex) => {
33            if disposition == ExceptionDisposition::Handled {
34                ex.handle_error();
35                Ok(ex)
36            } else {
37                // Propagate or Continued — steps execute for side-effects (e.g. logging) but
38                // the modified exchange is discarded and the original error propagated.
39                Err(original_err)
40            }
41        }
42        Err(_) => {
43            // log-policy: handler-owned
44            tracing::warn!(error = %original_err, "on_steps pipeline failed, falling back to handler/DLC");
45            let mut ex = snapshot;
46            ex.set_error(original_err);
47            send_to_handler(ex, handler).await
48        }
49    }
50}
51
52/// Invoke a processor: readiness check + call, unified into a single Result.
53///
54/// Readiness errors and call errors are both returned as `Err(CamelError)`,
55/// allowing the pipeline's recovery loop to handle them uniformly.
56pub async fn invoke_processor(
57    svc: &mut BoxProcessor,
58    ex: Exchange,
59) -> Result<Exchange, CamelError> {
60    match svc.ready().await {
61        Ok(ready) => ready.call(ex).await,
62        Err(err) => Err(err),
63    }
64}
65
66/// Route-level error handler owning ALL error handling logic.
67///
68/// Single owner of DLC, retry, onException policies. Called from
69/// `RouteChannelService` (boundary errors) and `run_steps` (step errors).
70#[async_trait::async_trait]
71pub trait RouteErrorHandler: Send + Sync {
72    /// Match a policy for the given error. Called once before retry.
73    fn match_policy(&self, err: &CamelError) -> Option<PolicyId>;
74
75    /// Phase 1: Retry the failed step.
76    async fn retry_step(
77        &self,
78        policy: Option<PolicyId>,
79        step: &mut dyn camel_api::error_handler::RetryableStep,
80        original: Exchange,
81        error: CamelError,
82    ) -> RetryOutcome;
83
84    /// Phase 2: Determine step disposition after retry exhaustion.
85    async fn handle_step(
86        &self,
87        policy: Option<PolicyId>,
88        exchange: Exchange,
89        error: CamelError,
90    ) -> Result<StepDisposition, CamelError>;
91
92    /// Handle boundary (infrastructure) errors.
93    async fn handle_boundary(
94        &self,
95        kind: BoundaryKind,
96        exchange: Exchange,
97        error: CamelError,
98    ) -> Result<Exchange, CamelError>;
99}
100
101/// Default implementation of RouteErrorHandler.
102/// Owns DLC producer exclusively. Encapsulates retry/onException/DLC logic.
103///
104/// Uses `SyncBoxProcessor` internally so the handler is `Send + Sync` as required
105/// by the `RouteErrorHandler` trait.
106pub struct DefaultRouteErrorHandler {
107    pub(crate) dlc_producer: Option<SyncBoxProcessor>,
108    pub(crate) policies: Vec<(ExceptionPolicy, Option<SyncBoxProcessor>)>,
109    /// When true, restore the original Message (body and headers, pre-route, pre-mutation)
110    /// before forwarding to the DLC/handler. The original message is stashed by
111    /// `RouteChannelService::call` as the `ORIGINAL_MESSAGE_EXTENSION` extension.
112    pub use_original_message: bool,
113}
114
115impl DefaultRouteErrorHandler {
116    pub fn new(
117        dlc_producer: Option<BoxProcessor>,
118        policies: Vec<(ExceptionPolicy, Option<BoxProcessor>)>,
119    ) -> Self {
120        Self {
121            dlc_producer: dlc_producer.map(SyncBoxProcessor::new),
122            policies: policies
123                .into_iter()
124                .map(|(p, prod)| (p, prod.map(SyncBoxProcessor::new)))
125                .collect(),
126            use_original_message: false,
127        }
128    }
129
130    /// Enable restoration of the original message before forwarding to DLC.
131    pub fn with_use_original_message(mut self, enabled: bool) -> Self {
132        self.use_original_message = enabled;
133        self
134    }
135
136    /// Restore the original message from the `CamelOriginalMessage` extension if
137    /// `use_original_message` is enabled. Called immediately before `send_to_handler`
138    /// dispatch in both `handle_step` and `handle_boundary`.
139    fn restore_original_message_if_enabled(&self, exchange: &mut Exchange) {
140        if self.use_original_message
141            && let Some(orig) =
142                exchange.get_extension::<camel_api::Message>(camel_api::ORIGINAL_MESSAGE_EXTENSION)
143        {
144            exchange.input = orig.clone();
145        }
146    }
147
148    /// Resolve (disposition, producer) for a matched policy.
149    /// Shared by handle_step and handle_boundary.
150    fn resolve_producer(
151        &self,
152        policy: Option<PolicyId>,
153    ) -> (ExceptionDisposition, Option<BoxProcessor>) {
154        match policy {
155            Some(PolicyId(idx)) => match self.policies.get(idx) {
156                Some((p, prod)) => (
157                    p.disposition,
158                    prod.as_ref()
159                        .map(|p| p.clone_inner())
160                        .or_else(|| self.dlc_producer.as_ref().map(|p| p.clone_inner())),
161                ),
162                None => (
163                    ExceptionDisposition::Propagate,
164                    self.dlc_producer.as_ref().map(|p| p.clone_inner()),
165                ),
166            },
167            None => (
168                ExceptionDisposition::Propagate,
169                self.dlc_producer.as_ref().map(|p| p.clone_inner()),
170            ),
171        }
172    }
173}
174
175#[async_trait::async_trait]
176impl RouteErrorHandler for DefaultRouteErrorHandler {
177    fn match_policy(&self, err: &CamelError) -> Option<PolicyId> {
178        self.policies
179            .iter()
180            .position(|(p, _)| (p.matches)(err))
181            .map(PolicyId)
182    }
183
184    async fn retry_step(
185        &self,
186        policy: Option<PolicyId>,
187        step: &mut dyn camel_api::error_handler::RetryableStep,
188        original: Exchange,
189        error: CamelError,
190    ) -> RetryOutcome {
191        let Some(PolicyId(idx)) = policy else {
192            return RetryOutcome::Exhausted {
193                exchange: original,
194                error,
195                policy: None,
196            };
197        };
198        let Some((policy_def, _)) = self.policies.get(idx) else {
199            return RetryOutcome::Exhausted {
200                exchange: original,
201                error,
202                policy,
203            };
204        };
205        let Some(ref backoff) = policy_def.retry else {
206            return RetryOutcome::Exhausted {
207                exchange: original,
208                error,
209                policy,
210            };
211        };
212
213        for attempt in 0..backoff.max_attempts {
214            let delay = backoff.delay_for(attempt);
215            tokio::time::sleep(delay).await;
216
217            let mut ex = original.clone();
218            ex.input.set_header(HEADER_REDELIVERED, Value::Bool(true));
219            ex.input.set_header(
220                HEADER_REDELIVERY_COUNTER,
221                Value::Number((attempt + 1).into()),
222            );
223            ex.input.set_header(
224                HEADER_REDELIVERY_MAX_COUNTER,
225                Value::Number(backoff.max_attempts.into()),
226            );
227
228            match step.invoke(ex).await {
229                PipelineOutcome::Completed(exchange) => {
230                    return RetryOutcome::Recovered(exchange);
231                }
232                PipelineOutcome::Stopped(stopped_ex) => {
233                    return RetryOutcome::Stopped(stopped_ex);
234                }
235                PipelineOutcome::Failed(retry_err) => {
236                    if attempt + 1 == backoff.max_attempts {
237                        let mut final_ex = original;
238                        final_ex
239                            .input
240                            .set_header(HEADER_REDELIVERED, Value::Bool(true));
241                        final_ex.input.set_header(
242                            HEADER_REDELIVERY_COUNTER,
243                            Value::Number(backoff.max_attempts.into()),
244                        );
245                        final_ex.input.set_header(
246                            HEADER_REDELIVERY_MAX_COUNTER,
247                            Value::Number(backoff.max_attempts.into()),
248                        );
249                        return RetryOutcome::Exhausted {
250                            exchange: final_ex,
251                            error: retry_err,
252                            policy,
253                        };
254                    }
255                }
256            }
257        }
258
259        RetryOutcome::Exhausted {
260            exchange: original,
261            error,
262            policy,
263        }
264    }
265
266    async fn handle_step(
267        &self,
268        policy: Option<PolicyId>,
269        mut exchange: Exchange,
270        error: CamelError,
271    ) -> Result<StepDisposition, CamelError> {
272        let (disposition, producer) = self.resolve_producer(policy);
273
274        // rc-fu1of: a non-matching policy with no DLC silently propagates —
275        // the operator sees "handler never ran" with no signal why. Emit a
276        // diagnostic naming the error kind so `kind:` vocabulary gaps are
277        // discoverable. debug! (not warn!): non-matching is an expected,
278        // selective-policy path, and this fires per failed exchange.
279        if policy.is_none() && producer.is_none() {
280            tracing::debug!(
281                kind = %error.variant_name(),
282                "no on_exceptions policy matched and no dead-letter channel configured; propagating error"
283            );
284        }
285
286        // Run on_steps if present (using the SAME policy identified by PolicyId).
287        // Skip on_steps for Propagate disposition to prevent double side-effects:
288        // on_steps results would be discarded (snapshot restored), and the DLC
289        // handler fires next — causing duplicate message production.
290        if !matches!(disposition, ExceptionDisposition::Propagate)
291            && let Some(PolicyId(idx)) = policy
292            && let Some((p, _)) = self.policies.get(idx)
293            && let Some(ref steps) = p.on_steps
294        {
295            let snapshot = exchange.clone();
296            exchange.set_error(error.clone());
297            let mut step_pipeline = steps.clone_inner();
298            let step_result = async {
299                let svc = step_pipeline.ready().await?;
300                svc.call(exchange).await
301            }
302            .await;
303            match step_result {
304                Ok(mut ex) => match disposition {
305                    ExceptionDisposition::Handled => {
306                        ex.handle_error();
307                        return Ok(StepDisposition::Handled(ex));
308                    }
309                    ExceptionDisposition::Continued => {
310                        ex.clear_error();
311                        return Ok(StepDisposition::Continued(ex));
312                    }
313                    // Propagate and any future variant restore the snapshot
314                    // and fall through to the DLC/handler path.
315                    _ => {
316                        exchange = snapshot;
317                    }
318                },
319                Err(_) => {
320                    exchange = snapshot;
321                }
322            }
323        }
324
325        // No on_steps, on_steps failed, or Propagate — forward to DLC/handler.
326        // BIND the returned exchange (must use handler output).
327        self.restore_original_message_if_enabled(&mut exchange);
328        exchange.set_error(error.clone());
329        match send_to_handler(exchange, producer).await {
330            Ok(handler_ex) => match disposition {
331                ExceptionDisposition::Handled => {
332                    let mut ex = handler_ex;
333                    ex.clear_error();
334                    Ok(StepDisposition::Handled(ex))
335                }
336                ExceptionDisposition::Continued => {
337                    let mut ex = handler_ex;
338                    ex.clear_error();
339                    Ok(StepDisposition::Continued(ex))
340                }
341                // Propagate and any future variant forward the error.
342                _ => Ok(StepDisposition::Propagate(error)),
343            },
344            // Dead code by construction: send_to_handler always returns Ok.
345            Err(_) => Ok(StepDisposition::Propagate(error)),
346        }
347    }
348
349    async fn handle_boundary(
350        &self,
351        _kind: BoundaryKind,
352        mut exchange: Exchange,
353        error: CamelError,
354    ) -> Result<Exchange, CamelError> {
355        // Boundary errors: match policy, run on_steps, forward to DLC.
356        // Disposition mapping:
357        //   Handled → clear error, return Ok(exchange)
358        //   Propagate | Continued → forward to DLC, return Ok(exchange_with_error)
359        //   (Continued at boundary = Propagate — no next step to continue to)
360        let policy = self.match_policy(&error);
361        let (disposition, producer) = self.resolve_producer(policy);
362
363        // Run on_steps if present (shared logic with handle_step).
364        // Skip on_steps for Propagate/Continued disposition to prevent double
365        // side-effects: on_steps results would be discarded (snapshot restored),
366        // and the DLC handler fires next — causing duplicate message production.
367        // At boundary level, Continued maps to Propagate semantics.
368        if !matches!(
369            disposition,
370            ExceptionDisposition::Propagate | ExceptionDisposition::Continued
371        ) && let Some(PolicyId(idx)) = policy
372            && let Some((p, _)) = self.policies.get(idx)
373            && let Some(ref steps) = p.on_steps
374        {
375            let snapshot = exchange.clone();
376            exchange.set_error(error.clone());
377            let mut step_pipeline = steps.clone_inner();
378            let step_result = async {
379                let svc = step_pipeline.ready().await?;
380                svc.call(exchange).await
381            }
382            .await;
383            match step_result {
384                Ok(mut ex) => match disposition {
385                    ExceptionDisposition::Handled => {
386                        ex.handle_error();
387                        return Ok(ex);
388                    }
389                    // Propagate | Continued and any future variant restore the snapshot.
390                    _ => {
391                        exchange = snapshot;
392                    }
393                },
394                Err(_) => {
395                    exchange = snapshot;
396                }
397            }
398        }
399
400        // Forward to DLC/handler — BIND returned exchange
401        self.restore_original_message_if_enabled(&mut exchange);
402        exchange.set_error(error.clone());
403        match send_to_handler(exchange, producer).await {
404            Ok(handler_ex) => match disposition {
405                ExceptionDisposition::Handled => {
406                    let mut ex = handler_ex;
407                    ex.clear_error();
408                    Ok(ex)
409                }
410                // Propagate | Continued and any future variant forward the error.
411                _ => {
412                    let mut ex = handler_ex;
413                    ex.set_error(error);
414                    Ok(ex)
415                }
416            },
417            // Dead code by construction: send_to_handler always returns Ok.
418            Err(e) => Err(e),
419        }
420    }
421}
422
423/// Tower Layer that wraps a pipeline with error handling behaviour.
424///
425/// Constructed with already-resolved producers; URI resolution happens in `camel-core`.
426pub struct ErrorHandlerLayer {
427    /// Resolved DLC producer (None = log only).
428    dlc_producer: Option<BoxProcessor>,
429    /// Policies with their resolved `handled_by` producers.
430    policies: Vec<(ExceptionPolicy, Option<BoxProcessor>)>,
431}
432
433impl ErrorHandlerLayer {
434    /// Create the layer with pre-resolved producers.
435    pub fn new(
436        dlc_producer: Option<BoxProcessor>,
437        policies: Vec<(ExceptionPolicy, Option<BoxProcessor>)>,
438    ) -> Self {
439        Self {
440            dlc_producer,
441            policies,
442        }
443    }
444}
445
446impl<S> Layer<S> for ErrorHandlerLayer
447where
448    S: Service<Exchange, Response = Exchange, Error = CamelError> + Send + Clone + 'static,
449    S::Future: Send + 'static,
450{
451    type Service = ErrorHandlerService<S>;
452
453    fn layer(&self, inner: S) -> Self::Service {
454        ErrorHandlerService {
455            inner,
456            dlc_producer: self.dlc_producer.clone(),
457            policies: self
458                .policies
459                .iter()
460                .map(|(p, prod)| (p.clone(), prod.clone()))
461                .collect(),
462        }
463    }
464}
465
466/// Tower Service that absorbs pipeline errors by retrying and/or forwarding to a DLC.
467///
468/// `call` always returns `Ok` — errors are absorbed. The returned exchange will have
469/// `has_error() == true` if the pipeline ultimately failed.
470pub struct ErrorHandlerService<S> {
471    inner: S,
472    dlc_producer: Option<BoxProcessor>,
473    policies: Vec<(ExceptionPolicy, Option<BoxProcessor>)>,
474}
475
476impl<S: Clone> Clone for ErrorHandlerService<S> {
477    fn clone(&self) -> Self {
478        Self {
479            inner: self.inner.clone(),
480            dlc_producer: self.dlc_producer.clone(),
481            policies: self
482                .policies
483                .iter()
484                .map(|(p, prod)| (p.clone(), prod.clone()))
485                .collect(),
486        }
487    }
488}
489
490impl<S> ErrorHandlerService<S>
491where
492    S: Service<Exchange, Response = Exchange, Error = CamelError> + Send + Clone + 'static,
493    S::Future: Send + 'static,
494{
495    /// Create the service directly (used in unit tests; in production use the Layer).
496    pub fn new(
497        inner: S,
498        dlc_producer: Option<BoxProcessor>,
499        policies: Vec<(ExceptionPolicy, Option<BoxProcessor>)>,
500    ) -> Self {
501        Self {
502            inner,
503            dlc_producer,
504            policies,
505        }
506    }
507}
508
509impl<S> Service<Exchange> for ErrorHandlerService<S>
510where
511    S: Service<Exchange, Response = Exchange, Error = CamelError> + Send + Clone + 'static,
512    S::Future: Send + 'static,
513{
514    type Response = Exchange;
515    type Error = CamelError;
516    type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
517
518    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
519        // Preserve backpressure (Pending) but never leak readiness errors upward.
520        // Readiness errors are deferred to call(), where they go through the same
521        // retry/onException/DLC path as call() errors. This is safe because call()
522        // re-checks readiness on a fresh inner clone via inner.ready().await.
523        match self.inner.poll_ready(cx) {
524            Poll::Pending => Poll::Pending,
525            Poll::Ready(Err(_)) => Poll::Ready(Ok(())),
526            Poll::Ready(Ok(())) => Poll::Ready(Ok(())),
527        }
528    }
529
530    fn call(&mut self, exchange: Exchange) -> Self::Future {
531        let mut inner = self.inner.clone();
532        let dlc = self.dlc_producer.clone();
533        let policies: Vec<(ExceptionPolicy, Option<BoxProcessor>)> = self
534            .policies
535            .iter()
536            .map(|(p, prod)| (p.clone(), prod.clone()))
537            .collect();
538
539        Box::pin(async move {
540            let original = exchange.clone();
541            let result = match inner.ready().await {
542                Ok(svc) => svc.call(exchange).await,
543                Err(e) => Err(e), // readiness error — enters retry/DLC/onException path below
544            };
545
546            let err = match result {
547                Ok(ex) => return Ok(ex),
548                Err(e) => e,
549            };
550
551            // Find the first matching policy.
552            let matched = policies.into_iter().find(|(p, _)| (p.matches)(&err));
553
554            if let Some((policy, policy_producer)) = matched {
555                // Retry if configured.
556                if let Some(ref backoff) = policy.retry {
557                    for attempt in 0..backoff.max_attempts {
558                        let delay = backoff.delay_for(attempt);
559                        tokio::time::sleep(delay).await;
560
561                        // Set redelivery headers
562                        let mut ex = original.clone();
563                        ex.input.set_header(HEADER_REDELIVERED, Value::Bool(true));
564                        ex.input.set_header(
565                            HEADER_REDELIVERY_COUNTER,
566                            Value::Number((attempt + 1).into()),
567                        );
568                        ex.input.set_header(
569                            HEADER_REDELIVERY_MAX_COUNTER,
570                            Value::Number(backoff.max_attempts.into()),
571                        );
572
573                        let result = match inner.ready().await {
574                            Ok(svc) => svc.call(ex).await,
575                            Err(e) => Err(e), // readiness error — enters retry exhaustion path
576                        };
577                        match result {
578                            Ok(ex) => return Ok(ex),
579                            Err(retry_err) => {
580                                if attempt + 1 == backoff.max_attempts {
581                                    // Retries exhausted — send to handler.
582                                    let mut original = original.clone();
583                                    original
584                                        .input
585                                        .set_header(HEADER_REDELIVERED, Value::Bool(true));
586                                    original.input.set_header(
587                                        HEADER_REDELIVERY_COUNTER,
588                                        Value::Number(backoff.max_attempts.into()),
589                                    );
590                                    original.input.set_header(
591                                        HEADER_REDELIVERY_MAX_COUNTER,
592                                        Value::Number(backoff.max_attempts.into()),
593                                    );
594                                    if let Some(ref steps) = policy.on_steps {
595                                        let handler = policy_producer.clone().or(dlc.clone());
596                                        return execute_on_steps(
597                                            original,
598                                            retry_err,
599                                            steps,
600                                            policy.disposition,
601                                            handler,
602                                        )
603                                        .await;
604                                    }
605                                    original.set_error(retry_err);
606                                    let handler = policy_producer.or(dlc);
607                                    return send_to_handler(original, handler).await;
608                                }
609                            }
610                        }
611                    }
612                }
613                // No retry configured (or 0 attempts) — send to policy handler or DLC.
614                if let Some(ref steps) = policy.on_steps {
615                    let handler = policy_producer.or(dlc);
616                    return execute_on_steps(original, err, steps, policy.disposition, handler)
617                        .await;
618                }
619                let mut ex = original.clone();
620                ex.set_error(err);
621                let handler = policy_producer.or(dlc);
622                send_to_handler(ex, handler).await
623            } else {
624                // No matching policy — forward directly to DLC.
625                let mut ex = original;
626                ex.set_error(err);
627                send_to_handler(ex, dlc).await
628            }
629        })
630    }
631}
632
633async fn send_to_handler(
634    exchange: Exchange,
635    producer: Option<BoxProcessor>,
636) -> Result<Exchange, CamelError> {
637    match producer {
638        None => {
639            // log-policy: system-broken
640            tracing::error!(
641                error = ?exchange.error,
642                "Exchange failed with no error handler configured"
643            );
644            Ok(exchange)
645        }
646        Some(mut prod) => match prod.ready().await {
647            Err(e) => {
648                // log-policy: system-broken
649                tracing::error!("DLC/handler not ready: {e}");
650                Ok(exchange)
651            }
652            Ok(svc) => match svc.call(exchange.clone()).await {
653                Ok(ex) => Ok(ex),
654                Err(e) => {
655                    // log-policy: system-broken
656                    tracing::error!("DLC/handler call failed: {e}");
657                    // Return the original exchange with original error intact.
658                    Ok(exchange)
659                }
660            },
661        },
662    }
663}
664
665#[cfg(test)]
666mod tests {
667    use super::*;
668    use camel_api::{
669        BoxProcessor, BoxProcessorExt, CamelError, Exchange, Message, OutcomePipeline,
670        OutcomeSegment, PipelineOutcome, RetryableStep, SyncBoxProcessor, Value,
671        error_handler::RedeliveryPolicy,
672    };
673    use std::future::Future;
674    use std::pin::Pin;
675    use std::sync::{
676        Arc,
677        atomic::{AtomicU32, Ordering},
678    };
679    use std::time::Duration;
680    use tower::ServiceExt;
681
682    fn make_exchange() -> Exchange {
683        Exchange::new(Message::new("test"))
684    }
685
686    fn failing_processor() -> BoxProcessor {
687        BoxProcessor::from_fn(|_ex| {
688            Box::pin(async { Err(CamelError::ProcessorError("boom".into())) })
689        })
690    }
691
692    fn ok_processor() -> BoxProcessor {
693        BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) }))
694    }
695
696    fn fail_n_times(n: u32) -> BoxProcessor {
697        let count = Arc::new(AtomicU32::new(0));
698        BoxProcessor::from_fn(move |ex| {
699            let count = Arc::clone(&count);
700            Box::pin(async move {
701                let c = count.fetch_add(1, Ordering::SeqCst);
702                if c < n {
703                    Err(CamelError::ProcessorError(format!("attempt {c}")))
704                } else {
705                    Ok(ex)
706                }
707            })
708        })
709    }
710
711    #[tokio::test]
712    async fn test_ok_passthrough() {
713        let svc = ErrorHandlerService::new(ok_processor(), None, vec![]);
714        let result = svc.oneshot(make_exchange()).await;
715        assert!(result.is_ok());
716        assert!(!result.unwrap().has_error());
717    }
718
719    #[tokio::test]
720    async fn test_error_goes_to_dlc() {
721        let received = Arc::new(std::sync::Mutex::new(Vec::<Exchange>::new()));
722        let received_clone = Arc::clone(&received);
723        let dlc = BoxProcessor::from_fn(move |ex: Exchange| {
724            let r = Arc::clone(&received_clone);
725            Box::pin(async move {
726                r.lock().unwrap().push(ex.clone());
727                Ok(ex)
728            })
729        });
730
731        let svc = ErrorHandlerService::new(failing_processor(), Some(dlc), vec![]);
732        let result = svc.oneshot(make_exchange()).await;
733        assert!(result.is_ok());
734        let ex = result.unwrap();
735        assert!(ex.has_error());
736        assert_eq!(received.lock().unwrap().len(), 1);
737    }
738
739    #[tokio::test]
740    async fn test_retry_recovers() {
741        let inner = fail_n_times(2);
742        let policy = ExceptionPolicy {
743            matches: Arc::new(|_| true),
744            retry: Some(RedeliveryPolicy {
745                max_attempts: 3,
746                initial_delay: Duration::from_millis(1),
747                multiplier: 1.0,
748                max_delay: Duration::from_millis(10),
749                jitter_factor: 0.0,
750            }),
751            handled_by: None,
752            on_steps: None,
753            disposition: ExceptionDisposition::Propagate,
754        };
755        let svc = ErrorHandlerService::new(inner, None, vec![(policy, None)]);
756        let result = svc.oneshot(make_exchange()).await;
757        assert!(result.is_ok());
758        assert!(!result.unwrap().has_error());
759    }
760
761    #[tokio::test]
762    async fn test_retry_exhausted_goes_to_dlc() {
763        let inner = fail_n_times(10);
764        let received = Arc::new(std::sync::Mutex::new(0u32));
765        let received_clone = Arc::clone(&received);
766        let dlc = BoxProcessor::from_fn(move |ex: Exchange| {
767            let r = Arc::clone(&received_clone);
768            Box::pin(async move {
769                *r.lock().unwrap() += 1;
770                Ok(ex)
771            })
772        });
773        let policy = ExceptionPolicy {
774            matches: Arc::new(|_| true),
775            retry: Some(RedeliveryPolicy {
776                max_attempts: 2,
777                initial_delay: Duration::from_millis(1),
778                multiplier: 1.0,
779                max_delay: Duration::from_millis(10),
780                jitter_factor: 0.0,
781            }),
782            handled_by: None,
783            on_steps: None,
784            disposition: ExceptionDisposition::Propagate,
785        };
786        let svc = ErrorHandlerService::new(inner, Some(dlc), vec![(policy, None)]);
787        let result = svc.oneshot(make_exchange()).await;
788        assert!(result.is_ok());
789        assert!(result.unwrap().has_error());
790        assert_eq!(*received.lock().unwrap(), 1);
791    }
792
793    #[test]
794    fn test_poll_ready_delegates_to_inner() {
795        use std::sync::atomic::AtomicBool;
796
797        /// A service that returns `Pending` on the first `poll_ready`, then `Ready`.
798        #[derive(Clone)]
799        struct DelayedReadyService {
800            ready: Arc<AtomicBool>,
801        }
802
803        impl Service<Exchange> for DelayedReadyService {
804            type Response = Exchange;
805            type Error = CamelError;
806            type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
807
808            fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
809                if self.ready.fetch_or(true, Ordering::SeqCst) {
810                    // Already marked ready (second+ call) → Ready
811                    Poll::Ready(Ok(()))
812                } else {
813                    // First call → Pending, schedule a wake
814                    cx.waker().wake_by_ref();
815                    Poll::Pending
816                }
817            }
818
819            fn call(&mut self, ex: Exchange) -> Self::Future {
820                Box::pin(async move { Ok(ex) })
821            }
822        }
823
824        let waker = futures::task::noop_waker();
825        let mut cx = Context::from_waker(&waker);
826
827        let inner = DelayedReadyService {
828            ready: Arc::new(AtomicBool::new(false)),
829        };
830        let mut svc = ErrorHandlerService::new(inner, None, vec![]);
831
832        // First poll_ready: inner returns Pending, so ErrorHandlerService must too.
833        let first = Pin::new(&mut svc).poll_ready(&mut cx);
834        assert!(first.is_pending(), "expected Pending on first poll_ready");
835
836        // Second poll_ready: inner returns Ready, so ErrorHandlerService must too.
837        let second = Pin::new(&mut svc).poll_ready(&mut cx);
838        assert!(second.is_ready(), "expected Ready on second poll_ready");
839    }
840
841    #[tokio::test]
842    async fn test_no_matching_policy_uses_dlc() {
843        let received = Arc::new(std::sync::Mutex::new(0u32));
844        let received_clone = Arc::clone(&received);
845        let dlc = BoxProcessor::from_fn(move |ex: Exchange| {
846            let r = Arc::clone(&received_clone);
847            Box::pin(async move {
848                *r.lock().unwrap() += 1;
849                Ok(ex)
850            })
851        });
852        let policy = ExceptionPolicy::new(|e| matches!(e, CamelError::Io(_)));
853        let svc = ErrorHandlerService::new(failing_processor(), Some(dlc), vec![(policy, None)]);
854        let result = svc.oneshot(make_exchange()).await;
855        assert!(result.is_ok());
856        assert_eq!(*received.lock().unwrap(), 1);
857    }
858
859    #[tokio::test]
860    async fn test_redelivery_headers_are_set() {
861        use camel_api::error_handler::{
862            HEADER_REDELIVERED, HEADER_REDELIVERY_COUNTER, HEADER_REDELIVERY_MAX_COUNTER,
863            RedeliveryPolicy,
864        };
865
866        let inner = fail_n_times(10);
867        let received = Arc::new(std::sync::Mutex::new(None));
868        let received_clone = Arc::clone(&received);
869        let dlc = BoxProcessor::from_fn(move |ex: Exchange| {
870            let r = Arc::clone(&received_clone);
871            Box::pin(async move {
872                *r.lock().unwrap() = Some(ex.clone());
873                Ok(ex)
874            })
875        });
876
877        let policy = ExceptionPolicy {
878            matches: Arc::new(|_| true),
879            retry: Some(RedeliveryPolicy {
880                max_attempts: 2,
881                initial_delay: Duration::from_millis(1),
882                multiplier: 1.0,
883                max_delay: Duration::from_millis(10),
884                jitter_factor: 0.0,
885            }),
886            handled_by: None,
887            on_steps: None,
888            disposition: ExceptionDisposition::Propagate,
889        };
890
891        let svc = ErrorHandlerService::new(inner, Some(dlc), vec![(policy, None)]);
892        let _ = svc.oneshot(make_exchange()).await.unwrap();
893
894        let ex = received.lock().unwrap().take().unwrap();
895        assert_eq!(
896            ex.input.header(HEADER_REDELIVERED),
897            Some(&Value::Bool(true))
898        );
899        assert_eq!(
900            ex.input.header(HEADER_REDELIVERY_COUNTER),
901            Some(&Value::Number(2.into()))
902        );
903        assert_eq!(
904            ex.input.header(HEADER_REDELIVERY_MAX_COUNTER),
905            Some(&Value::Number(2.into()))
906        );
907    }
908
909    #[tokio::test]
910    async fn test_jitter_produces_varying_delays_in_retry_flow() {
911        use std::time::Instant;
912
913        let inner = fail_n_times(10);
914        let received = Arc::new(std::sync::Mutex::new(None));
915        let received_clone = Arc::clone(&received);
916        let dlc = BoxProcessor::from_fn(move |ex: Exchange| {
917            let r = Arc::clone(&received_clone);
918            Box::pin(async move {
919                *r.lock().unwrap() = Some(ex.clone());
920                Ok(ex)
921            })
922        });
923
924        let policy = ExceptionPolicy {
925            matches: Arc::new(|_| true),
926            retry: Some(RedeliveryPolicy {
927                max_attempts: 5,
928                initial_delay: Duration::from_millis(20),
929                multiplier: 1.0,
930                max_delay: Duration::from_millis(100),
931                jitter_factor: 0.5,
932            }),
933            handled_by: None,
934            on_steps: None,
935            disposition: ExceptionDisposition::Propagate,
936        };
937
938        let start = Instant::now();
939        let svc = ErrorHandlerService::new(inner, Some(dlc), vec![(policy, None)]);
940        let _ = svc.oneshot(make_exchange()).await.unwrap();
941        let elapsed = start.elapsed();
942
943        assert!(
944            received.lock().unwrap().is_some(),
945            "DLC should have received exchange"
946        );
947
948        assert!(
949            elapsed >= Duration::from_millis(50),
950            "5 retries with 20ms base delay should take at least 50ms (with jitter low bound)"
951        );
952
953        assert!(
954            elapsed <= Duration::from_millis(500),
955            "5 retries with 20ms base delay + 50% jitter should not exceed 500ms"
956        );
957    }
958
959    #[tokio::test]
960    async fn test_on_steps_handled_true_consumes_error() {
961        use tower::ServiceExt;
962
963        let steps_pipeline = BoxProcessor::new(tower::service_fn(|mut ex: Exchange| {
964            ex.input.body = camel_api::Body::Bytes("handled".into());
965            async move { Ok(ex) }
966        }));
967        let policy = ExceptionPolicy {
968            matches: Arc::new(|_| true),
969            retry: None,
970            handled_by: None,
971            on_steps: Some(SyncBoxProcessor::new(steps_pipeline)),
972            disposition: ExceptionDisposition::Handled,
973        };
974        let inner = tower::service_fn(|_ex: Exchange| async {
975            Err::<Exchange, CamelError>(CamelError::RouteError("fail".to_string()))
976        });
977        let mut svc = ErrorHandlerService::new(inner, None, vec![(policy, None)]);
978        let ex = Exchange::default();
979        let result = svc.ready().await.unwrap().call(ex).await.unwrap();
980        assert!(result.error.is_none(), "handled:true should clear error");
981        assert!(matches!(result.input.body, camel_api::Body::Bytes(_)));
982    }
983
984    #[tokio::test]
985    async fn test_on_steps_handled_false_propagates_error() {
986        use tower::ServiceExt;
987
988        let steps_pipeline = BoxProcessor::new(tower::service_fn(|mut ex: Exchange| {
989            ex.input.body = camel_api::Body::Bytes("handled".into());
990            async move { Ok(ex) }
991        }));
992        let policy = ExceptionPolicy {
993            matches: Arc::new(|_| true),
994            retry: None,
995            handled_by: None,
996            on_steps: Some(SyncBoxProcessor::new(steps_pipeline)),
997            disposition: ExceptionDisposition::Propagate,
998        };
999        let inner = tower::service_fn(|_ex: Exchange| async {
1000            Err::<Exchange, CamelError>(CamelError::RouteError("fail".to_string()))
1001        });
1002        let mut svc = ErrorHandlerService::new(inner, None, vec![(policy, None)]);
1003        let ex = Exchange::default();
1004        let result = svc.ready().await.unwrap().call(ex).await;
1005        assert!(result.is_err(), "handled:false should propagate error");
1006    }
1007
1008    // --- Readiness error capture tests ---
1009    //
1010    // ErrorHandlerService must capture readiness errors (poll_ready returning Err)
1011    // and route them through retry/onException/DLC instead of propagating raw.
1012
1013    /// A service whose `poll_ready` always returns `Ready(Err(...))` but whose
1014    /// `call` returns `Ok`. This simulates a permanently-not-ready endpoint.
1015    #[derive(Clone)]
1016    struct ReadinessFailService {
1017        error: CamelError,
1018    }
1019
1020    impl ReadinessFailService {
1021        fn new(error: CamelError) -> Self {
1022            Self { error }
1023        }
1024    }
1025
1026    impl Service<Exchange> for ReadinessFailService {
1027        type Response = Exchange;
1028        type Error = CamelError;
1029        type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
1030
1031        fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
1032            Poll::Ready(Err(self.error.clone()))
1033        }
1034
1035        fn call(&mut self, ex: Exchange) -> Self::Future {
1036            // call() should never be reached if poll_ready returns Err,
1037            // but Tower's ready().await on a clone will re-encounter the readiness error.
1038            Box::pin(async move { Ok(ex) })
1039        }
1040    }
1041
1042    #[tokio::test]
1043    async fn test_readiness_error_goes_to_dlc() {
1044        let readiness_err = CamelError::ProcessorError("readiness-fail".into());
1045        let inner = ReadinessFailService {
1046            error: readiness_err,
1047        };
1048
1049        let received = Arc::new(std::sync::Mutex::new(Vec::<Exchange>::new()));
1050        let received_clone = Arc::clone(&received);
1051        let dlc = BoxProcessor::from_fn(move |ex: Exchange| {
1052            let r = Arc::clone(&received_clone);
1053            Box::pin(async move {
1054                r.lock().unwrap().push(ex.clone());
1055                Ok(ex)
1056            })
1057        });
1058
1059        let svc = ErrorHandlerService::new(inner, Some(dlc), vec![]);
1060        let result = svc.oneshot(make_exchange()).await;
1061
1062        // The error must be absorbed (Ok), not propagated raw (Err).
1063        assert!(
1064            result.is_ok(),
1065            "readiness error should be captured and sent to DLC, got: {:?}",
1066            result
1067        );
1068        let ex = result.unwrap();
1069        assert!(ex.has_error(), "exchange should carry the readiness error");
1070        assert_eq!(
1071            received.lock().unwrap().len(),
1072            1,
1073            "DLC should have received the exchange exactly once"
1074        );
1075    }
1076
1077    #[tokio::test]
1078    async fn test_readiness_error_goes_to_matching_policy() {
1079        let readiness_err = CamelError::ProcessorError("readiness-fail".into());
1080        let inner = ReadinessFailService {
1081            error: readiness_err,
1082        };
1083
1084        let steps_pipeline = BoxProcessor::new(tower::service_fn(|mut ex: Exchange| {
1085            ex.input.body = camel_api::Body::Bytes("handled-readiness".into());
1086            async move { Ok(ex) }
1087        }));
1088        let policy = ExceptionPolicy {
1089            matches: Arc::new(|_| true),
1090            retry: None,
1091            handled_by: None,
1092            on_steps: Some(SyncBoxProcessor::new(steps_pipeline)),
1093            disposition: ExceptionDisposition::Handled,
1094        };
1095
1096        let svc = ErrorHandlerService::new(inner, None, vec![(policy, None)]);
1097        let result = svc.oneshot(make_exchange()).await;
1098
1099        // The error must be absorbed and routed to the on_steps handler.
1100        assert!(
1101            result.is_ok(),
1102            "readiness error should be captured by policy, got: {:?}",
1103            result
1104        );
1105        let ex = result.unwrap();
1106        assert!(ex.error.is_none(), "handled:true should clear error");
1107        assert!(
1108            matches!(ex.input.body, camel_api::Body::Bytes(_)),
1109            "on_steps should have modified the body"
1110        );
1111    }
1112
1113    #[test]
1114    fn test_poll_ready_converts_readiness_error_to_ok() {
1115        let readiness_err = CamelError::ProcessorError("readiness-fail".into());
1116        let inner = ReadinessFailService {
1117            error: readiness_err,
1118        };
1119        let mut svc = ErrorHandlerService::new(inner, None, vec![]);
1120
1121        let waker = futures::task::noop_waker();
1122        let mut cx = Context::from_waker(&waker);
1123
1124        // poll_ready must NOT propagate the readiness error — convert to Ok.
1125        let poll = Pin::new(&mut svc).poll_ready(&mut cx);
1126        match poll {
1127            Poll::Ready(Ok(())) => { /* correct */ }
1128            Poll::Ready(Err(e)) => panic!("poll_ready leaked readiness error: {:?}", e),
1129            Poll::Pending => panic!("poll_ready should be Ready for readiness errors"),
1130        }
1131    }
1132
1133    // --- invoke_processor tests ---
1134
1135    #[tokio::test]
1136    async fn test_invoke_processor_returns_ok_on_success() {
1137        let mut svc = ok_processor();
1138        let ex = make_exchange();
1139        let result = invoke_processor(&mut svc, ex).await;
1140        assert!(result.is_ok());
1141    }
1142
1143    #[tokio::test]
1144    async fn test_invoke_processor_captures_readiness_error() {
1145        let mut failing_ready: BoxProcessor = BoxProcessor::new(ReadinessFailService::new(
1146            CamelError::ProcessorError("not ready".into()),
1147        ));
1148        let ex = make_exchange();
1149        let result = invoke_processor(&mut failing_ready, ex).await;
1150        assert!(result.is_err());
1151    }
1152
1153    #[tokio::test]
1154    async fn test_on_steps_handled_true_clears_exception_properties() {
1155        use tower::ServiceExt;
1156
1157        let steps_pipeline = BoxProcessor::new(tower::service_fn(|mut ex: Exchange| {
1158            ex.input.body = camel_api::Body::Bytes("handled".into());
1159            async move { Ok(ex) }
1160        }));
1161        let policy = ExceptionPolicy {
1162            matches: Arc::new(|_| true),
1163            retry: None,
1164            handled_by: None,
1165            on_steps: Some(SyncBoxProcessor::new(steps_pipeline)),
1166            disposition: ExceptionDisposition::Handled,
1167        };
1168        let inner = tower::service_fn(|_ex: Exchange| async {
1169            Err::<Exchange, CamelError>(CamelError::RouteError("fail".to_string()))
1170        });
1171        let mut svc = ErrorHandlerService::new(inner, None, vec![(policy, None)]);
1172        let ex = Exchange::default();
1173        let result = svc.ready().await.unwrap().call(ex).await.unwrap();
1174        assert!(result.error.is_none(), "handled:true should clear error");
1175        assert!(
1176            !result
1177                .properties
1178                .contains_key(camel_api::exchange::PROPERTY_EXCEPTION_MESSAGE),
1179            "handled:true should clear exception properties"
1180        );
1181        assert!(
1182            !result
1183                .properties
1184                .contains_key(camel_api::exchange::PROPERTY_EXCEPTION_KIND),
1185            "handled:true should clear exception kind property"
1186        );
1187        assert!(
1188            !result
1189                .properties
1190                .contains_key(camel_api::exchange::PROPERTY_EXCEPTION_CAUGHT),
1191            "handled:true should clear exception caught property"
1192        );
1193    }
1194
1195    // ── DefaultRouteErrorHandler tests ──
1196
1197    #[test]
1198    fn test_match_policy_returns_id_for_matching_error() {
1199        let handler = DefaultRouteErrorHandler::new(
1200            None,
1201            vec![(
1202                ExceptionPolicy::new(|e| matches!(e, CamelError::ProcessorError(_))),
1203                None,
1204            )],
1205        );
1206        let id = handler.match_policy(&CamelError::ProcessorError("test".into()));
1207        assert_eq!(id, Some(PolicyId(0)));
1208    }
1209
1210    #[test]
1211    fn test_match_policy_returns_none_for_unmatched() {
1212        let handler = DefaultRouteErrorHandler::new(None, vec![]);
1213        let id = handler.match_policy(&CamelError::ProcessorError("test".into()));
1214        assert_eq!(id, None);
1215    }
1216
1217    // ── retry_step tests ──
1218
1219    #[tokio::test]
1220    async fn test_retry_step_succeeds_on_second_attempt() {
1221        let mut policy = ExceptionPolicy::new(|_| true);
1222        policy.retry = Some(RedeliveryPolicy::new(3));
1223        let handler = DefaultRouteErrorHandler::new(None, vec![(policy, None)]);
1224        let mut step = fail_n_times(1); // fails once, then succeeds
1225        let ex = make_exchange();
1226        let outcome = handler
1227            .retry_step(
1228                Some(PolicyId(0)),
1229                &mut step,
1230                ex,
1231                CamelError::ProcessorError("attempt 0".into()),
1232            )
1233            .await;
1234        assert!(matches!(outcome, RetryOutcome::Recovered(_)));
1235    }
1236
1237    #[tokio::test]
1238    async fn test_retry_step_exhausted_when_all_fail() {
1239        let mut policy = ExceptionPolicy::new(|_| true);
1240        policy.retry = Some(RedeliveryPolicy::new(3));
1241        let handler = DefaultRouteErrorHandler::new(None, vec![(policy, None)]);
1242        let mut step = failing_processor();
1243        let ex = make_exchange();
1244        let outcome = handler
1245            .retry_step(
1246                Some(PolicyId(0)),
1247                &mut step,
1248                ex,
1249                CamelError::ProcessorError("boom".into()),
1250            )
1251            .await;
1252        assert!(matches!(outcome, RetryOutcome::Exhausted { .. }));
1253    }
1254
1255    #[tokio::test]
1256    async fn test_retry_step_no_policy_returns_exhausted_immediately() {
1257        let handler = DefaultRouteErrorHandler::new(None, vec![]);
1258        let mut step = ok_processor();
1259        let ex = make_exchange();
1260        let outcome = handler
1261            .retry_step(
1262                None,
1263                &mut step,
1264                ex,
1265                CamelError::ProcessorError("boom".into()),
1266            )
1267            .await;
1268        assert!(matches!(
1269            outcome,
1270            RetryOutcome::Exhausted { policy: None, .. }
1271        ));
1272    }
1273
1274    // ── handle_step tests ──
1275
1276    #[tokio::test]
1277    async fn test_handle_step_propagate_sends_to_dlc() {
1278        let dlc = BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) }));
1279        let handler = DefaultRouteErrorHandler::new(Some(dlc), vec![]);
1280        let ex = make_exchange();
1281        let result = handler
1282            .handle_step(None, ex, CamelError::ProcessorError("boom".into()))
1283            .await;
1284        assert!(matches!(result, Ok(StepDisposition::Propagate(_))));
1285    }
1286
1287    #[tokio::test]
1288    async fn test_handle_step_handled_uses_handler_output() {
1289        let handler_producer = BoxProcessor::from_fn(|mut ex| {
1290            Box::pin(async move {
1291                ex.input.set_header("processed_by", Value::Bool(true));
1292                Ok(ex)
1293            })
1294        });
1295        let policy = ExceptionPolicy {
1296            matches: std::sync::Arc::new(|_| true),
1297            retry: None,
1298            handled_by: None,
1299            on_steps: None,
1300            disposition: ExceptionDisposition::Handled,
1301        };
1302        let handler = DefaultRouteErrorHandler::new(None, vec![(policy, Some(handler_producer))]);
1303        let mut ex = make_exchange();
1304        ex.set_error(CamelError::ProcessorError("boom".into()));
1305        let result = handler
1306            .handle_step(
1307                Some(PolicyId(0)),
1308                ex,
1309                CamelError::ProcessorError("boom".into()),
1310            )
1311            .await;
1312        match result {
1313            Ok(StepDisposition::Handled(ex)) => {
1314                assert!(!ex.has_error(), "error should be cleared");
1315                assert_eq!(
1316                    ex.input.header("processed_by"),
1317                    Some(&Value::Bool(true)),
1318                    "should use handler's output exchange"
1319                );
1320            }
1321            other => panic!("expected Handled, got {:?}", other.is_ok()),
1322        }
1323    }
1324
1325    #[tokio::test]
1326    async fn test_handle_step_continued_clears_error() {
1327        let policy = ExceptionPolicy {
1328            matches: std::sync::Arc::new(|_| true),
1329            retry: None,
1330            handled_by: None,
1331            on_steps: None,
1332            disposition: ExceptionDisposition::Continued,
1333        };
1334        let handler = DefaultRouteErrorHandler::new(None, vec![(policy, None)]);
1335        let mut ex = make_exchange();
1336        ex.set_error(CamelError::ProcessorError("boom".into()));
1337        let result = handler
1338            .handle_step(
1339                Some(PolicyId(0)),
1340                ex,
1341                CamelError::ProcessorError("boom".into()),
1342            )
1343            .await;
1344        match result {
1345            Ok(StepDisposition::Continued(ex)) => assert!(!ex.has_error()),
1346            other => panic!("expected Continued, got {:?}", other.is_ok()),
1347        }
1348    }
1349
1350    #[tokio::test]
1351    async fn test_handle_step_with_on_steps_handled() {
1352        let steps_pipeline = BoxProcessor::new(tower::service_fn(|mut ex: Exchange| {
1353            ex.input.body = camel_api::Body::Bytes("on_steps_ran".into());
1354            async move { Ok(ex) }
1355        }));
1356        let policy = ExceptionPolicy {
1357            matches: std::sync::Arc::new(|_| true),
1358            retry: None,
1359            handled_by: None,
1360            on_steps: Some(SyncBoxProcessor::new(steps_pipeline)),
1361            disposition: ExceptionDisposition::Handled,
1362        };
1363        let handler = DefaultRouteErrorHandler::new(None, vec![(policy, None)]);
1364        let mut ex = make_exchange();
1365        ex.set_error(CamelError::ProcessorError("boom".into()));
1366        let result = handler
1367            .handle_step(
1368                Some(PolicyId(0)),
1369                ex,
1370                CamelError::ProcessorError("boom".into()),
1371            )
1372            .await;
1373        match result {
1374            Ok(StepDisposition::Handled(ex)) => {
1375                assert!(!ex.has_error(), "error should be cleared");
1376                assert!(
1377                    matches!(ex.input.body, camel_api::Body::Bytes(_)),
1378                    "on_steps should have modified the body"
1379                );
1380            }
1381            other => panic!("expected Handled, got: {}", other.is_ok()),
1382        }
1383    }
1384
1385    #[tokio::test]
1386    async fn test_handle_step_dlc_failure_propagates() {
1387        let failing_dlc = BoxProcessor::from_fn(|_| {
1388            Box::pin(async { Err(CamelError::ProcessorError("dlc broken".into())) })
1389        });
1390        let handler = DefaultRouteErrorHandler::new(Some(failing_dlc), vec![]);
1391        let ex = make_exchange();
1392        let result = handler
1393            .handle_step(None, ex, CamelError::ProcessorError("original".into()))
1394            .await;
1395        assert!(
1396            matches!(result, Ok(StepDisposition::Propagate(_))),
1397            "DLC failure should still return Propagate with original error"
1398        );
1399    }
1400
1401    // ── handle_boundary tests ──
1402
1403    #[tokio::test]
1404    async fn test_handle_boundary_security_error_goes_to_dlc() {
1405        let dlc_count = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
1406        let count_clone = dlc_count.clone();
1407        let dlc = BoxProcessor::from_fn(move |ex| {
1408            let c = count_clone.clone();
1409            Box::pin(async move {
1410                c.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1411                Ok(ex)
1412            })
1413        });
1414        let handler = DefaultRouteErrorHandler::new(Some(dlc), vec![]);
1415        let ex = make_exchange();
1416        let result = handler
1417            .handle_boundary(
1418                BoundaryKind::Security,
1419                ex,
1420                CamelError::Unauthorized("denied".into()),
1421            )
1422            .await;
1423        assert!(result.is_ok());
1424        assert_eq!(dlc_count.load(std::sync::atomic::Ordering::SeqCst), 1);
1425    }
1426
1427    #[tokio::test]
1428    async fn test_handle_boundary_handled_clears_error() {
1429        let policy = ExceptionPolicy {
1430            matches: std::sync::Arc::new(|_| true),
1431            retry: None,
1432            handled_by: None,
1433            on_steps: None,
1434            disposition: ExceptionDisposition::Handled,
1435        };
1436        let handler = DefaultRouteErrorHandler::new(None, vec![(policy, None)]);
1437        let ex = make_exchange();
1438        let result = handler
1439            .handle_boundary(
1440                BoundaryKind::Security,
1441                ex,
1442                CamelError::Unauthorized("denied".into()),
1443            )
1444            .await;
1445        assert!(result.is_ok());
1446        assert!(
1447            !result.unwrap().has_error(),
1448            "Handled disposition should clear error"
1449        );
1450    }
1451
1452    #[tokio::test]
1453    async fn test_handle_boundary_propagate_preserves_error() {
1454        let policy = ExceptionPolicy {
1455            matches: std::sync::Arc::new(|_| true),
1456            retry: None,
1457            handled_by: None,
1458            on_steps: None,
1459            disposition: ExceptionDisposition::Propagate,
1460        };
1461        let handler = DefaultRouteErrorHandler::new(None, vec![(policy, None)]);
1462        let ex = make_exchange();
1463        let result = handler
1464            .handle_boundary(
1465                BoundaryKind::CircuitBreaker,
1466                ex,
1467                CamelError::CircuitOpen("open".into()),
1468            )
1469            .await;
1470        assert!(result.is_ok(), "boundary errors always return Ok");
1471        assert!(
1472            result.unwrap().has_error(),
1473            "Propagate disposition should preserve error"
1474        );
1475    }
1476
1477    #[tokio::test]
1478    async fn test_handle_boundary_continued_preserves_error_like_propagate() {
1479        let dlc_count = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
1480        let count_clone = dlc_count.clone();
1481        let dlc = BoxProcessor::from_fn(move |ex| {
1482            let c = count_clone.clone();
1483            Box::pin(async move {
1484                c.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1485                Ok(ex)
1486            })
1487        });
1488        let policy = ExceptionPolicy {
1489            matches: std::sync::Arc::new(|_| true),
1490            retry: None,
1491            handled_by: None,
1492            on_steps: None,
1493            disposition: ExceptionDisposition::Continued,
1494        };
1495        let handler = DefaultRouteErrorHandler::new(Some(dlc), vec![(policy, None)]);
1496        let ex = make_exchange();
1497        let result = handler
1498            .handle_boundary(
1499                BoundaryKind::Security,
1500                ex,
1501                CamelError::Unauthorized("denied".into()),
1502            )
1503            .await;
1504        assert!(result.is_ok(), "boundary errors always return Ok");
1505        assert!(
1506            result.unwrap().has_error(),
1507            "Continued at boundary should preserve error"
1508        );
1509        assert_eq!(
1510            dlc_count.load(std::sync::atomic::Ordering::SeqCst),
1511            1,
1512            "DLC should be called"
1513        );
1514    }
1515
1516    #[tokio::test]
1517    async fn test_handle_boundary_with_on_steps_handled() {
1518        let steps_pipeline = BoxProcessor::new(tower::service_fn(|mut ex: Exchange| {
1519            ex.input.body = camel_api::Body::Bytes("on_steps_ran".into());
1520            async move { Ok(ex) }
1521        }));
1522        let policy = ExceptionPolicy {
1523            matches: std::sync::Arc::new(|_| true),
1524            retry: None,
1525            handled_by: None,
1526            on_steps: Some(SyncBoxProcessor::new(steps_pipeline)),
1527            disposition: ExceptionDisposition::Handled,
1528        };
1529        let handler = DefaultRouteErrorHandler::new(None, vec![(policy, None)]);
1530        let ex = make_exchange();
1531        let result = handler
1532            .handle_boundary(
1533                BoundaryKind::Security,
1534                ex,
1535                CamelError::Unauthorized("denied".into()),
1536            )
1537            .await;
1538        assert!(result.is_ok(), "boundary errors always return Ok");
1539        let ex = result.unwrap();
1540        assert!(!ex.has_error(), "Handled disposition should clear error");
1541        assert!(
1542            matches!(ex.input.body, camel_api::Body::Bytes(_)),
1543            "on_steps should have modified the body"
1544        );
1545    }
1546
1547    #[tokio::test]
1548    async fn retry_step_segment_stop_maps_to_retry_outcome_stopped() {
1549        use std::sync::Arc;
1550        use std::sync::atomic::{AtomicUsize, Ordering};
1551
1552        #[derive(Clone)]
1553        struct StoppingSegment {
1554            n: Arc<AtomicUsize>,
1555        }
1556        impl OutcomePipeline for StoppingSegment {
1557            fn clone_box(&self) -> Box<dyn OutcomePipeline> {
1558                Box::new(self.clone())
1559            }
1560            fn run<'a>(
1561                &'a mut self,
1562                ex: Exchange,
1563            ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
1564                let n = self.n.clone();
1565                Box::pin(async move {
1566                    n.fetch_add(1, Ordering::SeqCst);
1567                    PipelineOutcome::Stopped(ex)
1568                })
1569            }
1570        }
1571
1572        let call_count = Arc::new(AtomicUsize::new(0));
1573        let seg = OutcomeSegment::new(Box::new(StoppingSegment {
1574            n: call_count.clone(),
1575        }));
1576        let mut retryable: Box<dyn RetryableStep> = Box::new(seg);
1577
1578        let mut policy = ExceptionPolicy::new(|_e: &CamelError| true);
1579        policy.retry = Some(RedeliveryPolicy::new(3));
1580        let handler = DefaultRouteErrorHandler::new(None, vec![(policy, None)]);
1581
1582        let original = Exchange::new(Message::new("retry-me"));
1583        let err = CamelError::ProcessorError("trigger retry".into());
1584        let outcome = handler
1585            .retry_step(Some(PolicyId(0)), retryable.as_mut(), original, err)
1586            .await;
1587
1588        assert!(
1589            matches!(outcome, RetryOutcome::Stopped(_)),
1590            "Segment Stop must map to RetryOutcome::Stopped, got {:?}",
1591            outcome
1592        );
1593        assert_eq!(
1594            call_count.load(Ordering::SeqCst),
1595            1,
1596            "Stop must short-circuit retry — only 1 invoke expected, got {}",
1597            call_count.load(Ordering::SeqCst)
1598        );
1599    }
1600
1601    #[tokio::test]
1602    async fn retry_step_new_signature_works_with_dlc_producer() {
1603        use std::sync::Arc;
1604        use std::sync::atomic::{AtomicUsize, Ordering};
1605
1606        #[derive(Clone)]
1607        struct CountingProducer {
1608            count: Arc<AtomicUsize>,
1609            succeed_on: usize,
1610        }
1611        impl tower::Service<Exchange> for CountingProducer {
1612            type Response = Exchange;
1613            type Error = CamelError;
1614            type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
1615            fn poll_ready(
1616                &mut self,
1617                _cx: &mut std::task::Context<'_>,
1618            ) -> std::task::Poll<Result<(), Self::Error>> {
1619                std::task::Poll::Ready(Ok(()))
1620            }
1621            fn call(&mut self, ex: Exchange) -> Self::Future {
1622                let n = self.count.fetch_add(1, Ordering::SeqCst);
1623                let succeed_on = self.succeed_on;
1624                Box::pin(async move {
1625                    if n >= succeed_on {
1626                        Ok(ex)
1627                    } else {
1628                        Err(CamelError::ProcessorError("retry".into()))
1629                    }
1630                })
1631            }
1632        }
1633
1634        let count = Arc::new(AtomicUsize::new(0));
1635        let producer = CountingProducer {
1636            count: count.clone(),
1637            succeed_on: 2,
1638        };
1639        let sync_bp = SyncBoxProcessor::new(BoxProcessor::new(producer));
1640        let bp1 = sync_bp.clone_inner();
1641        let bp2 = sync_bp.clone_inner();
1642        let mut retryable1: Box<dyn RetryableStep> = Box::new(bp1);
1643        let mut retryable2: Box<dyn RetryableStep> = Box::new(bp2);
1644
1645        let ex = Exchange::new(Message::new("dlc"));
1646        let outcome1 = retryable1.invoke(ex.clone()).await;
1647        let outcome2 = retryable2.invoke(ex).await;
1648        assert!(matches!(outcome1, PipelineOutcome::Failed(_)));
1649        assert!(matches!(outcome2, PipelineOutcome::Failed(_)));
1650        assert_eq!(
1651            count.load(Ordering::SeqCst),
1652            2,
1653            "DLC producer must be invoked exactly twice through SyncBoxProcessor"
1654        );
1655        drop(retryable1);
1656        drop(retryable2);
1657        drop(sync_bp);
1658    }
1659
1660    // ── use_original_message tests ──
1661
1662    #[tokio::test]
1663    async fn test_use_original_message_restores_body_before_dlc() {
1664        // Verifies that when the extension is set, handle_step restores the
1665        // original message body before the DLC sees it.
1666        let dlc_received = Arc::new(std::sync::Mutex::new(None::<Exchange>));
1667        let dlc_received_clone = Arc::clone(&dlc_received);
1668        let dlc = BoxProcessor::from_fn(move |ex: Exchange| {
1669            let r = Arc::clone(&dlc_received_clone);
1670            Box::pin(async move {
1671                *r.lock().unwrap() = Some(ex.clone());
1672                Ok(ex)
1673            })
1674        });
1675
1676        let mut handler = DefaultRouteErrorHandler::new(Some(dlc), vec![]);
1677        handler.use_original_message = true;
1678
1679        // Build exchange with original body, stash it, then mutate.
1680        let mut ex = make_exchange();
1681        ex.input = Message::new("original-body");
1682
1683        // Simulate RouteChannelService stashing the original message.
1684        let original: Arc<Message> = Arc::new(ex.input.clone());
1685        ex.set_extension(camel_api::ORIGINAL_MESSAGE_EXTENSION, original);
1686
1687        // Mutate the body (simulating a pipeline step that transforms then fails).
1688        ex.input.body = camel_api::Body::Bytes("mutated-body".into());
1689
1690        // Call handle_step — the restore should fire before send_to_handler.
1691        let result = handler
1692            .handle_step(None, ex, CamelError::ProcessorError("boom".into()))
1693            .await;
1694        assert!(matches!(result, Ok(StepDisposition::Propagate(_))));
1695
1696        // The DLC must have received the exchange with the ORIGINAL body.
1697        let received = dlc_received
1698            .lock()
1699            .unwrap()
1700            .take()
1701            .expect("DLC should have been called");
1702        let received_text = match &received.input.body {
1703            camel_api::Body::Text(s) => s.clone(),
1704            camel_api::Body::Bytes(b) => String::from_utf8_lossy(b).to_string(),
1705            camel_api::Body::Json(v) => v.to_string(),
1706            _ => String::new(),
1707        };
1708        assert_eq!(
1709            received_text, "original-body",
1710            "DLC should receive original message body, not mutated version"
1711        );
1712    }
1713
1714    #[tokio::test]
1715    async fn test_use_original_message_handle_boundary_restores_before_dlc() {
1716        let dlc_received = Arc::new(std::sync::Mutex::new(None::<Exchange>));
1717        let dlc_received_clone = Arc::clone(&dlc_received);
1718        let dlc = BoxProcessor::from_fn(move |ex: Exchange| {
1719            let r = Arc::clone(&dlc_received_clone);
1720            Box::pin(async move {
1721                *r.lock().unwrap() = Some(ex.clone());
1722                Ok(ex)
1723            })
1724        });
1725
1726        let mut handler = DefaultRouteErrorHandler::new(Some(dlc), vec![]);
1727        handler.use_original_message = true;
1728
1729        let mut ex = make_exchange();
1730        ex.input = Message::new("orig-boundary");
1731
1732        let original: Arc<Message> = Arc::new(ex.input.clone());
1733        ex.set_extension(camel_api::ORIGINAL_MESSAGE_EXTENSION, original);
1734
1735        // Mutate body before boundary error.
1736        ex.input.body = camel_api::Body::Bytes("mutated-boundary".into());
1737
1738        let result = handler
1739            .handle_boundary(
1740                BoundaryKind::Security,
1741                ex,
1742                CamelError::Unauthorized("denied".into()),
1743            )
1744            .await;
1745        assert!(result.is_ok());
1746
1747        let received = dlc_received
1748            .lock()
1749            .unwrap()
1750            .take()
1751            .expect("DLC should have been called");
1752        let received_text = match &received.input.body {
1753            camel_api::Body::Text(s) => s.clone(),
1754            camel_api::Body::Bytes(b) => String::from_utf8_lossy(b).to_string(),
1755            camel_api::Body::Json(v) => v.to_string(),
1756            _ => String::new(),
1757        };
1758        assert_eq!(
1759            received_text, "orig-boundary",
1760            "handle_boundary should restore original message before sending to DLC"
1761        );
1762    }
1763
1764    #[tokio::test]
1765    async fn test_use_original_message_false_does_not_restore() {
1766        // When use_original_message is false (default), the mutation should PASS THROUGH.
1767        let dlc_received = Arc::new(std::sync::Mutex::new(None::<Exchange>));
1768        let dlc_received_clone = Arc::clone(&dlc_received);
1769        let dlc = BoxProcessor::from_fn(move |ex: Exchange| {
1770            let r = Arc::clone(&dlc_received_clone);
1771            Box::pin(async move {
1772                *r.lock().unwrap() = Some(ex.clone());
1773                Ok(ex)
1774            })
1775        });
1776
1777        let handler = DefaultRouteErrorHandler::new(Some(dlc), vec![]);
1778        // use_original_message defaults to false
1779
1780        let mut ex = make_exchange();
1781        ex.input = Message::new("original-body");
1782
1783        // Stash still set but flag is false — must be ignored.
1784        let original: Arc<Message> = Arc::new(ex.input.clone());
1785        ex.set_extension(camel_api::ORIGINAL_MESSAGE_EXTENSION, original);
1786
1787        // Mutate the body.
1788        ex.input.body = camel_api::Body::Bytes("mutated-body".into());
1789
1790        let result = handler
1791            .handle_step(None, ex, CamelError::ProcessorError("boom".into()))
1792            .await;
1793        assert!(matches!(result, Ok(StepDisposition::Propagate(_))));
1794
1795        let received = dlc_received
1796            .lock()
1797            .unwrap()
1798            .take()
1799            .expect("DLC should have been called");
1800        let received_text = match &received.input.body {
1801            camel_api::Body::Text(s) => s.clone(),
1802            camel_api::Body::Bytes(b) => String::from_utf8_lossy(b).to_string(),
1803            camel_api::Body::Json(v) => v.to_string(),
1804            _ => String::new(),
1805        };
1806        assert_eq!(
1807            received_text, "mutated-body",
1808            "When use_original_message=false, DLC should see the mutated body"
1809        );
1810    }
1811
1812    // ── D-M17: Propagate must skip on_steps (prevents double side-effects) ──
1813
1814    #[tokio::test]
1815    async fn propagate_skips_on_steps_in_handle_step() {
1816        let on_steps_called = Arc::new(AtomicU32::new(0));
1817        let on_steps_called_clone = on_steps_called.clone();
1818        let steps_pipeline = BoxProcessor::new(tower::service_fn(move |mut ex: Exchange| {
1819            let c = on_steps_called_clone.clone();
1820            Box::pin(async move {
1821                c.fetch_add(1, Ordering::SeqCst);
1822                ex.input.body = camel_api::Body::Bytes("on_steps_ran".into());
1823                Ok(ex)
1824            })
1825        }));
1826        let dlc_called = Arc::new(AtomicU32::new(0));
1827        let dlc_called_clone = dlc_called.clone();
1828        let dlc = BoxProcessor::from_fn(move |ex: Exchange| {
1829            let c = dlc_called_clone.clone();
1830            Box::pin(async move {
1831                c.fetch_add(1, Ordering::SeqCst);
1832                Ok(ex)
1833            })
1834        });
1835        let policy = ExceptionPolicy {
1836            matches: std::sync::Arc::new(|_| true),
1837            retry: None,
1838            handled_by: None,
1839            on_steps: Some(SyncBoxProcessor::new(steps_pipeline)),
1840            disposition: ExceptionDisposition::Propagate,
1841        };
1842        let handler = DefaultRouteErrorHandler::new(Some(dlc), vec![(policy, None)]);
1843        let mut ex = make_exchange();
1844        ex.set_error(CamelError::ProcessorError("boom".into()));
1845        let result = handler
1846            .handle_step(
1847                Some(PolicyId(0)),
1848                ex,
1849                CamelError::ProcessorError("boom".into()),
1850            )
1851            .await;
1852        assert!(
1853            matches!(result, Ok(StepDisposition::Propagate(_))),
1854            "Propagate disposition should return Propagate"
1855        );
1856        assert_eq!(
1857            on_steps_called.load(Ordering::SeqCst),
1858            0,
1859            "on_steps must NOT be called when disposition is Propagate (double side-effect bug)"
1860        );
1861        assert_eq!(
1862            dlc_called.load(Ordering::SeqCst),
1863            1,
1864            "DLC should still be called when disposition is Propagate"
1865        );
1866    }
1867
1868    #[tokio::test]
1869    async fn propagate_skips_on_steps_in_handle_boundary() {
1870        let on_steps_called = Arc::new(AtomicU32::new(0));
1871        let on_steps_called_clone = on_steps_called.clone();
1872        let steps_pipeline = BoxProcessor::new(tower::service_fn(move |mut ex: Exchange| {
1873            let c = on_steps_called_clone.clone();
1874            Box::pin(async move {
1875                c.fetch_add(1, Ordering::SeqCst);
1876                ex.input.body = camel_api::Body::Bytes("on_steps_ran".into());
1877                Ok(ex)
1878            })
1879        }));
1880        let dlc_called = Arc::new(AtomicU32::new(0));
1881        let dlc_called_clone = dlc_called.clone();
1882        let dlc = BoxProcessor::from_fn(move |ex: Exchange| {
1883            let c = dlc_called_clone.clone();
1884            Box::pin(async move {
1885                c.fetch_add(1, Ordering::SeqCst);
1886                Ok(ex)
1887            })
1888        });
1889        let policy = ExceptionPolicy {
1890            matches: std::sync::Arc::new(|_| true),
1891            retry: None,
1892            handled_by: None,
1893            on_steps: Some(SyncBoxProcessor::new(steps_pipeline)),
1894            disposition: ExceptionDisposition::Propagate,
1895        };
1896        let handler = DefaultRouteErrorHandler::new(Some(dlc), vec![(policy, None)]);
1897        let ex = make_exchange();
1898        let result = handler
1899            .handle_boundary(
1900                BoundaryKind::CircuitBreaker,
1901                ex,
1902                CamelError::CircuitOpen("open".into()),
1903            )
1904            .await;
1905        assert!(result.is_ok(), "boundary errors always return Ok");
1906        assert!(
1907            result.unwrap().has_error(),
1908            "Propagate disposition should preserve error"
1909        );
1910        assert_eq!(
1911            on_steps_called.load(Ordering::SeqCst),
1912            0,
1913            "on_steps must NOT be called when disposition is Propagate (double side-effect bug)"
1914        );
1915        assert_eq!(
1916            dlc_called.load(Ordering::SeqCst),
1917            1,
1918            "DLC should still be called when disposition is Propagate"
1919        );
1920    }
1921}