Skip to main content

camel_processor/
recipient_list.rs

1use std::future::Future;
2use std::pin::Pin;
3use std::task::{Context, Poll};
4
5use tokio::task::JoinSet;
6use tower::Service;
7use tower::ServiceExt;
8
9use camel_api::endpoint_pipeline::{CAMEL_SLIP_ENDPOINT, EndpointPipelineConfig};
10use camel_api::recipient_list::RecipientListConfig;
11use camel_api::{Body, CamelError, Exchange, Value};
12
13use crate::endpoint_pipeline::EndpointPipelineService;
14
15#[derive(Clone)]
16pub struct RecipientListService {
17    config: RecipientListConfig,
18    pipeline: EndpointPipelineService,
19}
20
21impl RecipientListService {
22    pub fn new(
23        config: RecipientListConfig,
24        endpoint_resolver: camel_api::EndpointResolver,
25    ) -> Result<Self, CamelError> {
26        config.validate()?;
27        let pipeline_config = EndpointPipelineConfig {
28            cache_size: EndpointPipelineConfig::from_signed(1000),
29            ignore_invalid_endpoints: false,
30        };
31        Ok(Self {
32            config,
33            pipeline: EndpointPipelineService::new(endpoint_resolver, pipeline_config),
34        })
35    }
36}
37
38impl Service<Exchange> for RecipientListService {
39    type Response = Exchange;
40    type Error = CamelError;
41    type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
42
43    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
44        Poll::Ready(Ok(()))
45    }
46
47    fn call(&mut self, mut exchange: Exchange) -> Self::Future {
48        let config = self.config.clone();
49        let pipeline = self.pipeline.clone();
50
51        Box::pin(async move {
52            let uris_raw = (config.expression)(&exchange);
53            if uris_raw.is_empty() {
54                return Ok(exchange);
55            }
56
57            // H13 Batch 1: cap the resolved-URI list BEFORE any endpoint
58            // resolution. A malicious expression yielding millions of URIs
59            // would otherwise allocate a Vec of millions of &str references
60            // and resolve each one (multicast) or call each one (sequential).
61            // The default cap is 1_000 (camel-api::recipient_list).
62            let cap = config.max_recipients;
63            let uris: Vec<&str> = uris_raw
64                .split(&config.delimiter)
65                .map(|s| s.trim())
66                .filter(|s| !s.is_empty())
67                .take(cap)
68                .collect();
69            if uris.is_empty() {
70                return Ok(exchange);
71            }
72
73            if config.parallel {
74                let original_for_aggregate = exchange.clone();
75                let mut endpoints_to_call = Vec::with_capacity(uris.len());
76                for uri in &uris {
77                    if let Some(endpoint) = pipeline.resolve(uri)? {
78                        endpoints_to_call.push((uri.to_string(), endpoint));
79                    }
80                }
81
82                let mut results: Vec<Exchange> = Vec::with_capacity(endpoints_to_call.len());
83                let mut join_set = JoinSet::new();
84                let mut iter = endpoints_to_call.into_iter();
85                let raw_limit = config.parallel_limit.unwrap_or(results.capacity());
86                let limit = raw_limit.max(1).min(results.capacity().max(1));
87
88                for _ in 0..limit {
89                    if let Some((uri, mut endpoint)) = iter.next() {
90                        let mut cloned = original_for_aggregate.clone();
91                        cloned.set_property(CAMEL_SLIP_ENDPOINT, Value::String(uri));
92                        join_set.spawn(async move { endpoint.ready().await?.call(cloned).await });
93                    }
94                }
95
96                while let Some(result) = join_set.join_next().await {
97                    match result {
98                        Ok(Ok(ex)) => results.push(ex),
99                        Ok(Err(e)) if config.stop_on_exception => {
100                            join_set.abort_all();
101                            return Err(e);
102                        }
103                        _ => {}
104                    }
105
106                    if let Some((uri, mut endpoint)) = iter.next() {
107                        let mut cloned = original_for_aggregate.clone();
108                        cloned.set_property(CAMEL_SLIP_ENDPOINT, Value::String(uri));
109                        join_set.spawn(async move { endpoint.ready().await?.call(cloned).await });
110                    }
111                }
112
113                exchange = aggregate_results(config.strategy, original_for_aggregate, results);
114            } else {
115                let mut results: Vec<Exchange> = Vec::new();
116                let original_for_aggregate = exchange.clone();
117                for uri in &uris {
118                    let endpoint = match pipeline.resolve(uri)? {
119                        Some(e) => e,
120                        None => continue,
121                    };
122                    exchange.set_property(CAMEL_SLIP_ENDPOINT, Value::String(uri.to_string()));
123                    let mut endpoint = endpoint;
124                    let result = endpoint.ready().await?.call(exchange.clone()).await;
125                    match result {
126                        Ok(ex) => {
127                            results.push(ex.clone());
128                            exchange = ex;
129                        }
130                        Err(e) if config.stop_on_exception => return Err(e),
131                        Err(_) => continue,
132                    }
133                }
134                exchange = aggregate_results(config.strategy, original_for_aggregate, results);
135            }
136
137            Ok(exchange)
138        })
139    }
140}
141
142fn aggregate_results(
143    strategy: camel_api::MulticastStrategy,
144    original: Exchange,
145    results: Vec<Exchange>,
146) -> Exchange {
147    match strategy {
148        camel_api::MulticastStrategy::LastWins => results.into_iter().last().unwrap_or(original),
149        camel_api::MulticastStrategy::Original => original,
150        camel_api::MulticastStrategy::CollectAll => {
151            let bodies: Vec<Value> = results
152                .iter()
153                .map(|ex| match &ex.input.body {
154                    Body::Text(s) => Value::String(s.clone()),
155                    Body::Json(v) => v.clone(),
156                    Body::Xml(s) => Value::String(s.clone()),
157                    Body::Bytes(b) => Value::String(String::from_utf8_lossy(b).into_owned()),
158                    Body::Empty => Value::Null,
159                    Body::Stream(s) => serde_json::json!({
160                        "_stream": {
161                            "origin": s.metadata.origin,
162                            "placeholder": true,
163                            "hint": "Materialize exchange body with .into_bytes() before recipient-list aggregation"
164                        }
165                    }),
166                })
167                .collect();
168            let mut result = results.into_iter().last().unwrap_or(original);
169            result.input.body = camel_api::Body::from(Value::Array(bodies));
170            result
171        }
172        camel_api::MulticastStrategy::Custom(fn_) => {
173            results.into_iter().fold(original, |acc, ex| fn_(acc, ex))
174        }
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use camel_api::MulticastStrategy;
182    use camel_api::{BoxProcessor, BoxProcessorExt, CamelError, Message};
183    use std::collections::HashMap;
184    use std::sync::Arc;
185    use std::sync::atomic::{AtomicUsize, Ordering};
186    use std::time::{Duration, Instant};
187    use tokio::sync::Mutex;
188    use tokio::time::sleep;
189
190    fn mock_resolver() -> camel_api::EndpointResolver {
191        Arc::new(|uri: &str| {
192            if uri.starts_with("mock:") {
193                Some(BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })))
194            } else {
195                None
196            }
197        })
198    }
199
200    #[tokio::test]
201    async fn recipient_list_single_destination() {
202        let call_count = Arc::new(AtomicUsize::new(0));
203        let count_clone = call_count.clone();
204
205        let resolver = Arc::new(move |uri: &str| {
206            if uri == "mock:a" {
207                let count = count_clone.clone();
208                Some(BoxProcessor::from_fn(move |ex| {
209                    count.fetch_add(1, Ordering::SeqCst);
210                    Box::pin(async move { Ok(ex) })
211                }))
212            } else {
213                None
214            }
215        });
216
217        let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| "mock:a".to_string()));
218
219        let mut svc = RecipientListService::new(config, resolver).unwrap();
220        let ex = Exchange::new(Message::new("test"));
221        let result = svc.ready().await.unwrap().call(ex).await;
222
223        assert!(result.is_ok());
224        assert_eq!(call_count.load(Ordering::SeqCst), 1);
225    }
226
227    #[tokio::test]
228    async fn recipient_list_multiple_destinations() {
229        let call_count = Arc::new(AtomicUsize::new(0));
230        let count_clone = call_count.clone();
231
232        let resolver = Arc::new(move |uri: &str| {
233            if uri.starts_with("mock:") {
234                let count = count_clone.clone();
235                Some(BoxProcessor::from_fn(move |ex| {
236                    count.fetch_add(1, Ordering::SeqCst);
237                    Box::pin(async move { Ok(ex) })
238                }))
239            } else {
240                None
241            }
242        });
243
244        let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
245            "mock:a,mock:b,mock:c".to_string()
246        }));
247
248        let mut svc = RecipientListService::new(config, resolver).unwrap();
249        let ex = Exchange::new(Message::new("test"));
250        let result = svc.ready().await.unwrap().call(ex).await;
251
252        assert!(result.is_ok());
253        assert_eq!(call_count.load(Ordering::SeqCst), 3);
254    }
255
256    #[tokio::test]
257    async fn recipient_list_empty_expression() {
258        let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| String::new()));
259
260        let mut svc = RecipientListService::new(config, mock_resolver()).unwrap();
261        let ex = Exchange::new(Message::new("test"));
262        let result = svc.ready().await.unwrap().call(ex).await;
263
264        assert!(result.is_ok());
265    }
266
267    #[tokio::test]
268    async fn recipient_list_invalid_endpoint_error() {
269        let config =
270            RecipientListConfig::new(Arc::new(|_ex: &Exchange| "invalid:endpoint".to_string()));
271
272        let mut svc = RecipientListService::new(config, mock_resolver()).unwrap();
273        let ex = Exchange::new(Message::new("test"));
274        let result = svc.ready().await.unwrap().call(ex).await;
275
276        assert!(result.is_err());
277        assert!(result.unwrap_err().to_string().contains("Invalid endpoint"));
278    }
279
280    #[tokio::test]
281    async fn recipient_list_custom_delimiter() {
282        use std::sync::Mutex;
283
284        let order: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
285
286        let resolver = {
287            let order = order.clone();
288            Arc::new(move |uri: &str| {
289                let order = order.clone();
290                let uri = uri.to_string();
291                Some(BoxProcessor::from_fn(move |ex| {
292                    order.lock().unwrap().push(uri.clone());
293                    Box::pin(async move { Ok(ex) })
294                }))
295            })
296        };
297
298        let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
299            "mock:x|mock:y|mock:z".to_string()
300        }))
301        .delimiter("|");
302
303        let mut svc = RecipientListService::new(config, resolver).unwrap();
304        let ex = Exchange::new(Message::new("test"));
305        svc.ready().await.unwrap().call(ex).await.unwrap();
306
307        let order = order.lock().unwrap();
308        assert_eq!(*order, vec!["mock:x", "mock:y", "mock:z"]);
309    }
310
311    #[tokio::test]
312    async fn recipient_list_expression_evaluated_once() {
313        let expr_count = Arc::new(AtomicUsize::new(0));
314        let expr_count_clone = expr_count.clone();
315
316        let config = RecipientListConfig::new(Arc::new(move |_ex: &Exchange| {
317            expr_count_clone.fetch_add(1, Ordering::SeqCst);
318            "mock:a,mock:b".to_string()
319        }));
320
321        let mut svc = RecipientListService::new(config, mock_resolver()).unwrap();
322        let ex = Exchange::new(Message::new("test"));
323        svc.ready().await.unwrap().call(ex).await.unwrap();
324
325        assert_eq!(
326            expr_count.load(Ordering::SeqCst),
327            1,
328            "Expression must be evaluated exactly once"
329        );
330    }
331
332    #[tokio::test]
333    async fn recipient_list_ignores_empty_uri_tokens() {
334        let call_count = Arc::new(AtomicUsize::new(0));
335        let call_count_clone = call_count.clone();
336
337        let resolver = Arc::new(move |uri: &str| {
338            if uri.starts_with("mock:") {
339                let count = call_count_clone.clone();
340                Some(BoxProcessor::from_fn(move |ex| {
341                    count.fetch_add(1, Ordering::SeqCst);
342                    Box::pin(async move { Ok(ex) })
343                }))
344            } else {
345                None
346            }
347        });
348
349        let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
350            " ,mock:a, ,mock:b,, ".to_string()
351        }));
352
353        let mut svc = RecipientListService::new(config, resolver).unwrap();
354        let ex = Exchange::new(Message::new("test"));
355        let result = svc.ready().await.unwrap().call(ex).await;
356        assert!(result.is_ok());
357        assert_eq!(call_count.load(Ordering::SeqCst), 2);
358    }
359
360    #[tokio::test]
361    async fn recipient_list_mutation_between_steps() {
362        let resolver = Arc::new(|uri: &str| {
363            if uri == "mock:mutate" {
364                Some(BoxProcessor::from_fn(|mut ex| {
365                    ex.input.body = camel_api::Body::Text("mutated".to_string());
366                    Box::pin(async move { Ok(ex) })
367                }))
368            } else if uri == "mock:verify" {
369                Some(BoxProcessor::from_fn(|ex| {
370                    let body = ex.input.body.as_text().unwrap_or("").to_string();
371                    assert_eq!(body, "mutated");
372                    Box::pin(async move { Ok(ex) })
373                }))
374            } else {
375                None
376            }
377        });
378
379        let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
380            "mock:mutate,mock:verify".to_string()
381        }));
382
383        let mut svc = RecipientListService::new(config, resolver).unwrap();
384        let ex = Exchange::new(Message::new("original"));
385        let result = svc.ready().await.unwrap().call(ex).await;
386
387        assert!(result.is_ok());
388    }
389
390    #[tokio::test]
391    async fn recipient_list_parallel_executes_concurrently() {
392        let records: Arc<Mutex<Vec<(String, Instant, Instant)>>> = Arc::new(Mutex::new(Vec::new()));
393
394        let resolver = {
395            let records = records.clone();
396            Arc::new(move |uri: &str| {
397                if uri.starts_with("mock:") {
398                    let records = records.clone();
399                    let uri = uri.to_string();
400                    Some(BoxProcessor::from_fn(move |ex| {
401                        let records = records.clone();
402                        let uri = uri.clone();
403                        Box::pin(async move {
404                            let start = Instant::now();
405                            sleep(Duration::from_millis(100)).await;
406                            let end = Instant::now();
407                            records.lock().await.push((uri, start, end));
408                            Ok(ex)
409                        })
410                    }))
411                } else {
412                    None
413                }
414            })
415        };
416
417        let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
418            "mock:a,mock:b,mock:c".to_string()
419        }))
420        .parallel(true);
421
422        let mut svc = RecipientListService::new(config, resolver).unwrap();
423        let ex = Exchange::new(Message::new("test"));
424        svc.ready().await.unwrap().call(ex).await.unwrap();
425
426        let records = records.lock().await;
427        assert_eq!(records.len(), 3);
428
429        let mut overlap_found = false;
430        for i in 0..records.len() {
431            for j in (i + 1)..records.len() {
432                let (_, a_start, a_end) = records[i];
433                let (_, b_start, b_end) = records[j];
434                if a_start < b_end && b_start < a_end {
435                    overlap_found = true;
436                    break;
437                }
438            }
439            if overlap_found {
440                break;
441            }
442        }
443
444        assert!(overlap_found);
445    }
446
447    #[tokio::test]
448    async fn recipient_list_parallel_stop_on_exception_returns_error() {
449        let resolver = Arc::new(|uri: &str| {
450            if uri == "mock:err" {
451                Some(BoxProcessor::from_fn(|_ex| {
452                    Box::pin(async { Err(CamelError::ProcessorError("boom".to_string())) })
453                }))
454            } else if uri.starts_with("mock:") {
455                Some(BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })))
456            } else {
457                None
458            }
459        });
460
461        let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
462            "mock:a,mock:err,mock:c".to_string()
463        }))
464        .parallel(true)
465        .stop_on_exception(true);
466
467        let mut svc = RecipientListService::new(config, resolver).unwrap();
468        let ex = Exchange::new(Message::new("test"));
469        let result = svc.ready().await.unwrap().call(ex).await;
470        assert!(matches!(result, Err(CamelError::ProcessorError(msg)) if msg == "boom"));
471    }
472
473    #[tokio::test]
474    async fn recipient_list_parallel_limit_respects_limit() {
475        let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
476            "mock:a,mock:b,mock:c,mock:d".to_string()
477        }))
478        .parallel(true)
479        .parallel_limit(2);
480
481        let resolver = Arc::new(|uri: &str| {
482            if uri.starts_with("mock:") {
483                Some(BoxProcessor::from_fn(|ex| {
484                    Box::pin(async move {
485                        sleep(Duration::from_millis(100)).await;
486                        Ok(ex)
487                    })
488                }))
489            } else {
490                None
491            }
492        });
493
494        let mut svc = RecipientListService::new(config, resolver).unwrap();
495        let ex = Exchange::new(Message::new("test"));
496        let start = Instant::now();
497        svc.ready().await.unwrap().call(ex).await.unwrap();
498        let elapsed = start.elapsed();
499
500        assert!(elapsed >= Duration::from_millis(180));
501        assert!(elapsed < Duration::from_millis(350));
502    }
503
504    #[tokio::test]
505    async fn recipient_list_collect_all_strategy() {
506        let resolver = Arc::new(|uri: &str| {
507            if uri == "mock:a" {
508                Some(BoxProcessor::from_fn(|mut ex| {
509                    ex.input.body = Body::Text("a".to_string());
510                    Box::pin(async move { Ok(ex) })
511                }))
512            } else if uri == "mock:b" {
513                Some(BoxProcessor::from_fn(|mut ex| {
514                    ex.input.body = Body::Text("b".to_string());
515                    Box::pin(async move { Ok(ex) })
516                }))
517            } else if uri == "mock:c" {
518                Some(BoxProcessor::from_fn(|mut ex| {
519                    ex.input.body = Body::Text("c".to_string());
520                    Box::pin(async move { Ok(ex) })
521                }))
522            } else {
523                None
524            }
525        });
526
527        let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
528            "mock:a,mock:b,mock:c".to_string()
529        }))
530        .strategy(MulticastStrategy::CollectAll);
531
532        let mut svc = RecipientListService::new(config, resolver).unwrap();
533        let ex = Exchange::new(Message::new("seed"));
534        let result = svc.ready().await.unwrap().call(ex).await.unwrap();
535
536        assert_eq!(
537            result.input.body,
538            Body::from(Value::Array(vec![
539                Value::String("a".to_string()),
540                Value::String("b".to_string()),
541                Value::String("c".to_string()),
542            ]))
543        );
544    }
545
546    #[tokio::test]
547    async fn recipient_list_original_strategy() {
548        let resolver = Arc::new(|uri: &str| {
549            if uri.starts_with("mock:") {
550                let label = uri.to_string();
551                Some(BoxProcessor::from_fn(move |mut ex| {
552                    let label = label.clone();
553                    ex.input.body = Body::Text(format!("mutated-{label}"));
554                    Box::pin(async move { Ok(ex) })
555                }))
556            } else {
557                None
558            }
559        });
560
561        let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
562            "mock:a,mock:b,mock:c".to_string()
563        }))
564        .strategy(MulticastStrategy::Original);
565
566        let mut svc = RecipientListService::new(config, resolver).unwrap();
567        let ex = Exchange::new(Message::new("original"));
568        let result = svc.ready().await.unwrap().call(ex).await.unwrap();
569
570        assert_eq!(result.input.body.as_text(), Some("original"));
571    }
572
573    // ── H13 Batch 1: cap resolved-URI count ──────────────────────────
574
575    /// H13: an expression yielding millions of URIs is truncated to
576    /// `max_recipients` before endpoint resolution. The test uses a
577    /// cap of 4 to keep the test fast; the principle (cap the list) is
578    /// what Batch 1 enforces. The default cap is 1_000 in camel-api.
579    #[tokio::test]
580    async fn test_huge_recipient_list_is_capped() {
581        let call_count = Arc::new(AtomicUsize::new(0));
582        let count_clone = call_count.clone();
583
584        let resolver = Arc::new(move |uri: &str| {
585            if uri.starts_with("mock:") {
586                let count = count_clone.clone();
587                Some(BoxProcessor::from_fn(move |ex| {
588                    count.fetch_add(1, Ordering::SeqCst);
589                    Box::pin(async move { Ok(ex) })
590                }))
591            } else {
592                None
593            }
594        });
595
596        // Build the untrusted payload: 1_000_000 URIs as one string.
597        let mut many = String::with_capacity(8 * 1_000_000);
598        for i in 0..1_000_000 {
599            if i > 0 {
600                many.push(',');
601            }
602            many.push_str(&format!("mock:k{i}"));
603        }
604
605        // Disposition-5 pattern: the untrusted data flows FROM the exchange
606        // (a header on the inbound message), NOT from a captured variable.
607        // The expression reads it off the passed `&Exchange` — this is what
608        // makes the cap an untrusted-data-validation control, not a local
609        // limit.
610        let config = RecipientListConfig::new(Arc::new(|ex: &Exchange| {
611            ex.input
612                .header("CamelRecipients")
613                .and_then(|v| v.as_str().map(|s| s.to_string()))
614                .unwrap_or_default()
615        }))
616        .max_recipients(4);
617
618        let mut svc = RecipientListService::new(config, resolver).unwrap();
619        let mut ex = Exchange::new(Message::new("test"));
620        ex.input.set_header("CamelRecipients", Value::String(many));
621        let result = svc.ready().await.unwrap().call(ex).await;
622        assert!(result.is_ok(), "capped execution should still succeed");
623        assert_eq!(
624            call_count.load(Ordering::SeqCst),
625            4,
626            "must resolve at most max_recipients (4) endpoints"
627        );
628    }
629
630    #[tokio::test]
631    async fn recipient_list_last_wins_strategy() {
632        let payloads: Arc<HashMap<String, String>> = Arc::new(HashMap::from([
633            ("mock:a".to_string(), "first".to_string()),
634            ("mock:b".to_string(), "second".to_string()),
635            ("mock:c".to_string(), "third".to_string()),
636        ]));
637
638        let resolver = {
639            let payloads = payloads.clone();
640            Arc::new(move |uri: &str| {
641                if let Some(payload) = payloads.get(uri) {
642                    let payload = payload.clone();
643                    Some(BoxProcessor::from_fn(move |mut ex| {
644                        let payload = payload.clone();
645                        ex.input.body = Body::Text(payload);
646                        Box::pin(async move { Ok(ex) })
647                    }))
648                } else {
649                    None
650                }
651            })
652        };
653
654        let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
655            "mock:a,mock:b,mock:c".to_string()
656        }))
657        .strategy(MulticastStrategy::LastWins);
658
659        let mut svc = RecipientListService::new(config, resolver).unwrap();
660        let ex = Exchange::new(Message::new("seed"));
661        let result = svc.ready().await.unwrap().call(ex).await.unwrap();
662
663        assert_eq!(result.input.body.as_text(), Some("third"));
664    }
665}