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::CollectAll => {
150            let bodies: Vec<Value> = results
151                .iter()
152                .map(|ex| match &ex.input.body {
153                    Body::Text(s) => Value::String(s.clone()),
154                    Body::Json(v) => v.clone(),
155                    Body::Xml(s) => Value::String(s.clone()),
156                    Body::Bytes(b) => Value::String(String::from_utf8_lossy(b).into_owned()),
157                    Body::Stream(s) => serde_json::json!({
158                        "_stream": {
159                            "origin": s.metadata.origin,
160                            "placeholder": true,
161                            "hint": "Materialize exchange body with .into_bytes() before recipient-list aggregation"
162                        }
163                    }),
164                    // Empty and future variants contribute no extractable value.
165                    _ => Value::Null,
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        // Original and any future variant return the original exchange.
176        _ => original,
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183    use camel_api::MulticastStrategy;
184    use camel_api::{BoxProcessor, BoxProcessorExt, CamelError, Message};
185    use std::collections::HashMap;
186    use std::sync::Arc;
187    use std::sync::atomic::{AtomicUsize, Ordering};
188    use std::time::{Duration, Instant};
189    use tokio::sync::Mutex;
190    use tokio::time::sleep;
191
192    fn mock_resolver() -> camel_api::EndpointResolver {
193        Arc::new(|uri: &str| {
194            if uri.starts_with("mock:") {
195                Some(BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })))
196            } else {
197                None
198            }
199        })
200    }
201
202    #[tokio::test]
203    async fn recipient_list_single_destination() {
204        let call_count = Arc::new(AtomicUsize::new(0));
205        let count_clone = call_count.clone();
206
207        let resolver = Arc::new(move |uri: &str| {
208            if uri == "mock:a" {
209                let count = count_clone.clone();
210                Some(BoxProcessor::from_fn(move |ex| {
211                    count.fetch_add(1, Ordering::SeqCst);
212                    Box::pin(async move { Ok(ex) })
213                }))
214            } else {
215                None
216            }
217        });
218
219        let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| "mock:a".to_string()));
220
221        let mut svc = RecipientListService::new(config, resolver).unwrap();
222        let ex = Exchange::new(Message::new("test"));
223        let result = svc.ready().await.unwrap().call(ex).await;
224
225        assert!(result.is_ok());
226        assert_eq!(call_count.load(Ordering::SeqCst), 1);
227    }
228
229    #[tokio::test]
230    async fn recipient_list_multiple_destinations() {
231        let call_count = Arc::new(AtomicUsize::new(0));
232        let count_clone = call_count.clone();
233
234        let resolver = Arc::new(move |uri: &str| {
235            if uri.starts_with("mock:") {
236                let count = count_clone.clone();
237                Some(BoxProcessor::from_fn(move |ex| {
238                    count.fetch_add(1, Ordering::SeqCst);
239                    Box::pin(async move { Ok(ex) })
240                }))
241            } else {
242                None
243            }
244        });
245
246        let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
247            "mock:a,mock:b,mock:c".to_string()
248        }));
249
250        let mut svc = RecipientListService::new(config, resolver).unwrap();
251        let ex = Exchange::new(Message::new("test"));
252        let result = svc.ready().await.unwrap().call(ex).await;
253
254        assert!(result.is_ok());
255        assert_eq!(call_count.load(Ordering::SeqCst), 3);
256    }
257
258    #[tokio::test]
259    async fn recipient_list_empty_expression() {
260        let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| String::new()));
261
262        let mut svc = RecipientListService::new(config, mock_resolver()).unwrap();
263        let ex = Exchange::new(Message::new("test"));
264        let result = svc.ready().await.unwrap().call(ex).await;
265
266        assert!(result.is_ok());
267    }
268
269    #[tokio::test]
270    async fn recipient_list_invalid_endpoint_error() {
271        let config =
272            RecipientListConfig::new(Arc::new(|_ex: &Exchange| "invalid:endpoint".to_string()));
273
274        let mut svc = RecipientListService::new(config, mock_resolver()).unwrap();
275        let ex = Exchange::new(Message::new("test"));
276        let result = svc.ready().await.unwrap().call(ex).await;
277
278        assert!(result.is_err());
279        assert!(result.unwrap_err().to_string().contains("Invalid endpoint"));
280    }
281
282    #[tokio::test]
283    async fn recipient_list_custom_delimiter() {
284        use std::sync::Mutex;
285
286        let order: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
287
288        let resolver = {
289            let order = order.clone();
290            Arc::new(move |uri: &str| {
291                let order = order.clone();
292                let uri = uri.to_string();
293                Some(BoxProcessor::from_fn(move |ex| {
294                    order.lock().unwrap().push(uri.clone());
295                    Box::pin(async move { Ok(ex) })
296                }))
297            })
298        };
299
300        let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
301            "mock:x|mock:y|mock:z".to_string()
302        }))
303        .delimiter("|");
304
305        let mut svc = RecipientListService::new(config, resolver).unwrap();
306        let ex = Exchange::new(Message::new("test"));
307        svc.ready().await.unwrap().call(ex).await.unwrap();
308
309        let order = order.lock().unwrap();
310        assert_eq!(*order, vec!["mock:x", "mock:y", "mock:z"]);
311    }
312
313    #[tokio::test]
314    async fn recipient_list_expression_evaluated_once() {
315        let expr_count = Arc::new(AtomicUsize::new(0));
316        let expr_count_clone = expr_count.clone();
317
318        let config = RecipientListConfig::new(Arc::new(move |_ex: &Exchange| {
319            expr_count_clone.fetch_add(1, Ordering::SeqCst);
320            "mock:a,mock:b".to_string()
321        }));
322
323        let mut svc = RecipientListService::new(config, mock_resolver()).unwrap();
324        let ex = Exchange::new(Message::new("test"));
325        svc.ready().await.unwrap().call(ex).await.unwrap();
326
327        assert_eq!(
328            expr_count.load(Ordering::SeqCst),
329            1,
330            "Expression must be evaluated exactly once"
331        );
332    }
333
334    #[tokio::test]
335    async fn recipient_list_ignores_empty_uri_tokens() {
336        let call_count = Arc::new(AtomicUsize::new(0));
337        let call_count_clone = call_count.clone();
338
339        let resolver = Arc::new(move |uri: &str| {
340            if uri.starts_with("mock:") {
341                let count = call_count_clone.clone();
342                Some(BoxProcessor::from_fn(move |ex| {
343                    count.fetch_add(1, Ordering::SeqCst);
344                    Box::pin(async move { Ok(ex) })
345                }))
346            } else {
347                None
348            }
349        });
350
351        let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
352            " ,mock:a, ,mock:b,, ".to_string()
353        }));
354
355        let mut svc = RecipientListService::new(config, resolver).unwrap();
356        let ex = Exchange::new(Message::new("test"));
357        let result = svc.ready().await.unwrap().call(ex).await;
358        assert!(result.is_ok());
359        assert_eq!(call_count.load(Ordering::SeqCst), 2);
360    }
361
362    #[tokio::test]
363    async fn recipient_list_mutation_between_steps() {
364        let resolver = Arc::new(|uri: &str| {
365            if uri == "mock:mutate" {
366                Some(BoxProcessor::from_fn(|mut ex| {
367                    ex.input.body = camel_api::Body::Text("mutated".to_string());
368                    Box::pin(async move { Ok(ex) })
369                }))
370            } else if uri == "mock:verify" {
371                Some(BoxProcessor::from_fn(|ex| {
372                    let body = ex.input.body.as_text().unwrap_or("").to_string();
373                    assert_eq!(body, "mutated");
374                    Box::pin(async move { Ok(ex) })
375                }))
376            } else {
377                None
378            }
379        });
380
381        let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
382            "mock:mutate,mock:verify".to_string()
383        }));
384
385        let mut svc = RecipientListService::new(config, resolver).unwrap();
386        let ex = Exchange::new(Message::new("original"));
387        let result = svc.ready().await.unwrap().call(ex).await;
388
389        assert!(result.is_ok());
390    }
391
392    #[tokio::test]
393    async fn recipient_list_parallel_executes_concurrently() {
394        let records: Arc<Mutex<Vec<(String, Instant, Instant)>>> = Arc::new(Mutex::new(Vec::new()));
395
396        let resolver = {
397            let records = records.clone();
398            Arc::new(move |uri: &str| {
399                if uri.starts_with("mock:") {
400                    let records = records.clone();
401                    let uri = uri.to_string();
402                    Some(BoxProcessor::from_fn(move |ex| {
403                        let records = records.clone();
404                        let uri = uri.clone();
405                        Box::pin(async move {
406                            let start = Instant::now();
407                            sleep(Duration::from_millis(100)).await;
408                            let end = Instant::now();
409                            records.lock().await.push((uri, start, end));
410                            Ok(ex)
411                        })
412                    }))
413                } else {
414                    None
415                }
416            })
417        };
418
419        let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
420            "mock:a,mock:b,mock:c".to_string()
421        }))
422        .parallel(true);
423
424        let mut svc = RecipientListService::new(config, resolver).unwrap();
425        let ex = Exchange::new(Message::new("test"));
426        svc.ready().await.unwrap().call(ex).await.unwrap();
427
428        let records = records.lock().await;
429        assert_eq!(records.len(), 3);
430
431        let mut overlap_found = false;
432        for i in 0..records.len() {
433            for j in (i + 1)..records.len() {
434                let (_, a_start, a_end) = records[i];
435                let (_, b_start, b_end) = records[j];
436                if a_start < b_end && b_start < a_end {
437                    overlap_found = true;
438                    break;
439                }
440            }
441            if overlap_found {
442                break;
443            }
444        }
445
446        assert!(overlap_found);
447    }
448
449    #[tokio::test]
450    async fn recipient_list_parallel_stop_on_exception_returns_error() {
451        let resolver = Arc::new(|uri: &str| {
452            if uri == "mock:err" {
453                Some(BoxProcessor::from_fn(|_ex| {
454                    Box::pin(async { Err(CamelError::ProcessorError("boom".to_string())) })
455                }))
456            } else if uri.starts_with("mock:") {
457                Some(BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })))
458            } else {
459                None
460            }
461        });
462
463        let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
464            "mock:a,mock:err,mock:c".to_string()
465        }))
466        .parallel(true)
467        .stop_on_exception(true);
468
469        let mut svc = RecipientListService::new(config, resolver).unwrap();
470        let ex = Exchange::new(Message::new("test"));
471        let result = svc.ready().await.unwrap().call(ex).await;
472        assert!(matches!(result, Err(CamelError::ProcessorError(msg)) if msg == "boom"));
473    }
474
475    #[tokio::test]
476    async fn recipient_list_parallel_limit_respects_limit() {
477        let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
478            "mock:a,mock:b,mock:c,mock:d".to_string()
479        }))
480        .parallel(true)
481        .parallel_limit(2);
482
483        let resolver = Arc::new(|uri: &str| {
484            if uri.starts_with("mock:") {
485                Some(BoxProcessor::from_fn(|ex| {
486                    Box::pin(async move {
487                        sleep(Duration::from_millis(100)).await;
488                        Ok(ex)
489                    })
490                }))
491            } else {
492                None
493            }
494        });
495
496        let mut svc = RecipientListService::new(config, resolver).unwrap();
497        let ex = Exchange::new(Message::new("test"));
498        let start = Instant::now();
499        svc.ready().await.unwrap().call(ex).await.unwrap();
500        let elapsed = start.elapsed();
501
502        assert!(elapsed >= Duration::from_millis(180));
503        assert!(elapsed < Duration::from_millis(350));
504    }
505
506    #[tokio::test]
507    async fn recipient_list_collect_all_strategy() {
508        let resolver = Arc::new(|uri: &str| {
509            if uri == "mock:a" {
510                Some(BoxProcessor::from_fn(|mut ex| {
511                    ex.input.body = Body::Text("a".to_string());
512                    Box::pin(async move { Ok(ex) })
513                }))
514            } else if uri == "mock:b" {
515                Some(BoxProcessor::from_fn(|mut ex| {
516                    ex.input.body = Body::Text("b".to_string());
517                    Box::pin(async move { Ok(ex) })
518                }))
519            } else if uri == "mock:c" {
520                Some(BoxProcessor::from_fn(|mut ex| {
521                    ex.input.body = Body::Text("c".to_string());
522                    Box::pin(async move { Ok(ex) })
523                }))
524            } else {
525                None
526            }
527        });
528
529        let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
530            "mock:a,mock:b,mock:c".to_string()
531        }))
532        .strategy(MulticastStrategy::CollectAll);
533
534        let mut svc = RecipientListService::new(config, resolver).unwrap();
535        let ex = Exchange::new(Message::new("seed"));
536        let result = svc.ready().await.unwrap().call(ex).await.unwrap();
537
538        assert_eq!(
539            result.input.body,
540            Body::from(Value::Array(vec![
541                Value::String("a".to_string()),
542                Value::String("b".to_string()),
543                Value::String("c".to_string()),
544            ]))
545        );
546    }
547
548    #[tokio::test]
549    async fn recipient_list_original_strategy() {
550        let resolver = Arc::new(|uri: &str| {
551            if uri.starts_with("mock:") {
552                let label = uri.to_string();
553                Some(BoxProcessor::from_fn(move |mut ex| {
554                    let label = label.clone();
555                    ex.input.body = Body::Text(format!("mutated-{label}"));
556                    Box::pin(async move { Ok(ex) })
557                }))
558            } else {
559                None
560            }
561        });
562
563        let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
564            "mock:a,mock:b,mock:c".to_string()
565        }))
566        .strategy(MulticastStrategy::Original);
567
568        let mut svc = RecipientListService::new(config, resolver).unwrap();
569        let ex = Exchange::new(Message::new("original"));
570        let result = svc.ready().await.unwrap().call(ex).await.unwrap();
571
572        assert_eq!(result.input.body.as_text(), Some("original"));
573    }
574
575    // ── H13 Batch 1: cap resolved-URI count ──────────────────────────
576
577    /// H13: an expression yielding millions of URIs is truncated to
578    /// `max_recipients` before endpoint resolution. The test uses a
579    /// cap of 4 to keep the test fast; the principle (cap the list) is
580    /// what Batch 1 enforces. The default cap is 1_000 in camel-api.
581    #[tokio::test]
582    async fn test_huge_recipient_list_is_capped() {
583        let call_count = Arc::new(AtomicUsize::new(0));
584        let count_clone = call_count.clone();
585
586        let resolver = Arc::new(move |uri: &str| {
587            if uri.starts_with("mock:") {
588                let count = count_clone.clone();
589                Some(BoxProcessor::from_fn(move |ex| {
590                    count.fetch_add(1, Ordering::SeqCst);
591                    Box::pin(async move { Ok(ex) })
592                }))
593            } else {
594                None
595            }
596        });
597
598        // Build the untrusted payload: 1_000_000 URIs as one string.
599        let mut many = String::with_capacity(8 * 1_000_000);
600        for i in 0..1_000_000 {
601            if i > 0 {
602                many.push(',');
603            }
604            many.push_str(&format!("mock:k{i}"));
605        }
606
607        // Disposition-5 pattern: the untrusted data flows FROM the exchange
608        // (a header on the inbound message), NOT from a captured variable.
609        // The expression reads it off the passed `&Exchange` — this is what
610        // makes the cap an untrusted-data-validation control, not a local
611        // limit.
612        let config = RecipientListConfig::new(Arc::new(|ex: &Exchange| {
613            ex.input
614                .header("CamelRecipients")
615                .and_then(|v| v.as_str().map(|s| s.to_string()))
616                .unwrap_or_default()
617        }))
618        .max_recipients(4);
619
620        let mut svc = RecipientListService::new(config, resolver).unwrap();
621        let mut ex = Exchange::new(Message::new("test"));
622        ex.input.set_header("CamelRecipients", Value::String(many));
623        let result = svc.ready().await.unwrap().call(ex).await;
624        assert!(result.is_ok(), "capped execution should still succeed");
625        assert_eq!(
626            call_count.load(Ordering::SeqCst),
627            4,
628            "must resolve at most max_recipients (4) endpoints"
629        );
630    }
631
632    #[tokio::test]
633    async fn recipient_list_last_wins_strategy() {
634        let payloads: Arc<HashMap<String, String>> = Arc::new(HashMap::from([
635            ("mock:a".to_string(), "first".to_string()),
636            ("mock:b".to_string(), "second".to_string()),
637            ("mock:c".to_string(), "third".to_string()),
638        ]));
639
640        let resolver = {
641            let payloads = payloads.clone();
642            Arc::new(move |uri: &str| {
643                if let Some(payload) = payloads.get(uri) {
644                    let payload = payload.clone();
645                    Some(BoxProcessor::from_fn(move |mut ex| {
646                        let payload = payload.clone();
647                        ex.input.body = Body::Text(payload);
648                        Box::pin(async move { Ok(ex) })
649                    }))
650                } else {
651                    None
652                }
653            })
654        };
655
656        let config = RecipientListConfig::new(Arc::new(|_ex: &Exchange| {
657            "mock:a,mock:b,mock:c".to_string()
658        }))
659        .strategy(MulticastStrategy::LastWins);
660
661        let mut svc = RecipientListService::new(config, resolver).unwrap();
662        let ex = Exchange::new(Message::new("seed"));
663        let result = svc.ready().await.unwrap().call(ex).await.unwrap();
664
665        assert_eq!(result.input.body.as_text(), Some("third"));
666    }
667}