Skip to main content

camel_api/
body.rs

1use crate::error::CamelError;
2/// General-purpose default limit for [`Body::materialize()`] (10 MB).
3///
4/// This is separate from `stream_cache::DEFAULT_STREAM_CACHE_THRESHOLD` (128 KB),
5/// which is the OOM-protection limit used by `StreamCacheService`.
6pub const DEFAULT_MATERIALIZE_LIMIT: usize = 10 * 1024 * 1024;
7
8use bytes::{Bytes, BytesMut};
9use futures::stream::BoxStream;
10use futures::{StreamExt, TryStreamExt};
11use std::io;
12use std::pin::Pin;
13use std::sync::Arc;
14use std::task::{Context as TaskContext, Poll};
15use tokio::io::{AsyncRead, ReadBuf};
16use tokio::sync::Mutex;
17use tokio_util::io::StreamReader;
18
19/// A boxed [`AsyncRead`] for reading body content without materializing it into memory.
20///
21/// Returned by [`Body::into_async_read()`].
22pub type BoxAsyncRead = Pin<Box<dyn AsyncRead + Send + Unpin>>;
23
24/// Metadata associated with a stream body.
25#[derive(Debug, Clone, Default)]
26pub struct StreamMetadata {
27    /// Expected size of the stream if known.
28    pub size_hint: Option<u64>,
29    /// Content type of the stream content.
30    pub content_type: Option<String>,
31    /// Origin of the stream (e.g. "file:///path/to/file").
32    pub origin: Option<String>,
33}
34
35/// A body that wraps a lazy-evaluated stream of bytes.
36///
37/// # Clone Semantics
38///
39/// The stream is **single-consumption**. When cloning a `Body::Stream`,
40/// all clones share the same underlying stream handle. Only the first
41/// clone to consume the stream will succeed; subsequent attempts will
42/// return `CamelError::AlreadyConsumed`.
43///
44/// # Example
45///
46/// ```rust
47/// use camel_api::{Body, StreamBody, error::CamelError};
48/// use futures::stream;
49/// use bytes::Bytes;
50/// use std::sync::Arc;
51/// use tokio::sync::Mutex;
52///
53/// # #[tokio::main]
54/// # async fn main() -> Result<(), CamelError> {
55/// let chunks = vec![Ok(Bytes::from("data"))];
56/// let stream = stream::iter(chunks);
57/// let body = Body::Stream(StreamBody {
58///     stream: Arc::new(Mutex::new(Some(Box::pin(stream)))),
59///     metadata: Default::default(),
60/// });
61///
62/// let clone = body.clone();
63///
64/// // First consumption succeeds
65/// let _ = body.into_bytes(1024).await?;
66///
67/// // Second consumption fails
68/// let result = clone.into_bytes(1024).await;
69/// assert!(matches!(result, Err(CamelError::AlreadyConsumed)));
70/// # Ok(())
71/// # }
72/// ```
73pub struct StreamBody {
74    /// The actual byte stream, wrapped in an Arc and Mutex to allow Clone for Body.
75    #[allow(clippy::type_complexity)]
76    pub stream: Arc<Mutex<Option<BoxStream<'static, Result<Bytes, CamelError>>>>>,
77    /// Metadata associated with the stream.
78    pub metadata: StreamMetadata,
79}
80
81impl std::fmt::Debug for StreamBody {
82    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83        f.debug_struct("StreamBody")
84            .field("metadata", &self.metadata)
85            .field("stream", &"<BoxStream>")
86            .finish()
87    }
88}
89
90impl Clone for StreamBody {
91    fn clone(&self) -> Self {
92        Self {
93            stream: Arc::clone(&self.stream),
94            metadata: self.metadata.clone(),
95        }
96    }
97}
98
99// ---------------------------------------------------------------------------
100// StreamAsyncRead — adapts Body::Stream into AsyncRead
101// ---------------------------------------------------------------------------
102
103/// Private adapter that implements [`AsyncRead`] for [`Body::Stream`].
104///
105/// On the first `poll_read`, attempts a non-blocking `try_lock()` on the inner
106/// `Arc<Mutex<Option<BoxStream>>>`:
107/// - If the lock succeeds and the stream is `Some`, extracts it and creates
108///   an active [`StreamReader`].
109/// - If the stream is `None` (already consumed), returns an [`io::Error`].
110/// - If the lock is contended (extremely rare), wakes the task and returns
111///   `Poll::Pending` to retry.
112#[allow(clippy::type_complexity)]
113struct StreamAsyncRead {
114    arc: Arc<Mutex<Option<BoxStream<'static, Result<Bytes, CamelError>>>>>,
115    /// Holds the active reader after the stream is extracted on first poll.
116    reader: Option<Box<dyn AsyncRead + Send + Unpin>>,
117    /// Set to true when the stream was already consumed (prevents further reads).
118    consumed: bool,
119}
120
121impl AsyncRead for StreamAsyncRead {
122    fn poll_read(
123        mut self: Pin<&mut Self>,
124        cx: &mut TaskContext<'_>,
125        buf: &mut ReadBuf<'_>,
126    ) -> Poll<io::Result<()>> {
127        if self.consumed {
128            return Poll::Ready(Err(io::Error::other("stream already consumed")));
129        }
130        // Lazy init: extract the stream on first poll
131        if self.reader.is_none() {
132            // Extract stream in a separate scope to avoid holding the lock while modifying self
133            let extracted = {
134                match self.arc.try_lock() {
135                    Ok(mut guard) => guard.take(),
136                    Err(_) => {
137                        // Lock contended — schedule a retry
138                        cx.waker().wake_by_ref();
139                        return Poll::Pending;
140                    }
141                }
142            };
143            // Now safe to modify self since lock is dropped
144            match extracted {
145                Some(stream) => {
146                    let mapped = stream.map_err(|e: CamelError| io::Error::other(e.to_string()));
147                    self.reader = Some(Box::new(StreamReader::new(mapped)));
148                }
149                None => {
150                    self.consumed = true;
151                    return Poll::Ready(Err(io::Error::other("stream already consumed")));
152                }
153            }
154        }
155        // Delegate to the active reader
156        Pin::new(self.reader.as_mut().unwrap()).poll_read(cx, buf) // allow-unwrap
157    }
158}
159
160/// The body of a message, supporting common payload types.
161#[derive(Debug, Default)]
162#[non_exhaustive]
163pub enum Body {
164    /// No body content.
165    #[default]
166    Empty,
167    /// Raw bytes payload.
168    Bytes(Bytes),
169    /// UTF-8 string payload.
170    Text(String),
171    /// JSON payload.
172    Json(serde_json::Value),
173    /// XML payload (well-formed XML string; use `try_into_xml()` for validation).
174    Xml(String),
175    /// Streaming payload.
176    Stream(StreamBody),
177}
178
179impl Clone for Body {
180    fn clone(&self) -> Self {
181        match self {
182            Body::Empty => Body::Empty,
183            Body::Bytes(b) => Body::Bytes(b.clone()),
184            Body::Text(s) => Body::Text(s.clone()),
185            Body::Json(v) => Body::Json(v.clone()),
186            Body::Xml(s) => Body::Xml(s.clone()),
187            Body::Stream(s) => Body::Stream(s.clone()),
188        }
189    }
190}
191
192impl PartialEq for Body {
193    fn eq(&self, other: &Self) -> bool {
194        match (self, other) {
195            (Body::Empty, Body::Empty) => true,
196            (Body::Text(a), Body::Text(b)) => a == b,
197            (Body::Json(a), Body::Json(b)) => a == b,
198            (Body::Bytes(a), Body::Bytes(b)) => a == b,
199            (Body::Xml(a), Body::Xml(b)) => a == b,
200            // Stream: two streams are never equal (single-consumption)
201            _ => false,
202        }
203    }
204}
205
206impl Body {
207    /// Returns `true` if the body is empty.
208    pub fn is_empty(&self) -> bool {
209        matches!(self, Body::Empty)
210    }
211
212    /// Convert the body into `Bytes`, consuming it if it is a stream.
213    /// This is an async operation because it may need to read from an underlying stream.
214    /// A `max_size` limit is enforced to prevent OOM errors.
215    pub async fn into_bytes(self, max_size: usize) -> Result<Bytes, CamelError> {
216        match self {
217            Body::Empty => Ok(Bytes::new()),
218            Body::Bytes(b) => {
219                if b.len() > max_size {
220                    return Err(CamelError::StreamLimitExceeded(max_size));
221                }
222                Ok(b)
223            }
224            Body::Text(s) => {
225                if s.len() > max_size {
226                    return Err(CamelError::StreamLimitExceeded(max_size));
227                }
228                Ok(Bytes::from(s))
229            }
230            Body::Json(v) => {
231                let b = serde_json::to_vec(&v)
232                    .map_err(|e| CamelError::TypeConversionFailed(e.to_string()))?;
233                if b.len() > max_size {
234                    return Err(CamelError::StreamLimitExceeded(max_size));
235                }
236                Ok(Bytes::from(b))
237            }
238            Body::Xml(s) => {
239                if s.len() > max_size {
240                    return Err(CamelError::StreamLimitExceeded(max_size));
241                }
242                Ok(Bytes::from(s))
243            }
244            Body::Stream(s) => {
245                let mut stream_lock = s.stream.lock().await;
246                let mut stream = stream_lock.take().ok_or(CamelError::AlreadyConsumed)?;
247
248                let mut buffer = BytesMut::new();
249                while let Some(chunk_res) = stream.next().await {
250                    let chunk = chunk_res?;
251                    if buffer.len() + chunk.len() > max_size {
252                        return Err(CamelError::StreamLimitExceeded(max_size));
253                    }
254                    buffer.extend_from_slice(&chunk);
255                }
256                Ok(buffer.freeze())
257            }
258        }
259    }
260
261    /// Materialize stream with sensible default limit (10 MB).
262    ///
263    /// Convenience method for common cases where you need the stream content
264    /// but don't want to specify a custom limit. For the tighter stream-cache
265    /// threshold (128 KB), use `stream_cache::DEFAULT_STREAM_CACHE_THRESHOLD`
266    /// with [`Body::into_bytes()`] instead.
267    ///
268    /// # Example
269    /// ```ignore
270    /// let body = Body::Stream(stream);
271    /// let bytes = body.materialize().await?;
272    /// ```
273    pub async fn materialize(self) -> Result<Bytes, CamelError> {
274        self.into_bytes(DEFAULT_MATERIALIZE_LIMIT).await
275    }
276
277    /// Convert the body into an [`AsyncRead`] without materializing it into memory.
278    ///
279    /// - [`Body::Empty`] → empty reader (0 bytes)
280    /// - [`Body::Bytes`] → in-memory cursor
281    /// - [`Body::Text`] → UTF-8 bytes cursor
282    /// - [`Body::Json`] → serialized JSON bytes cursor
283    /// - [`Body::Xml`] → UTF-8 bytes cursor
284    /// - [`Body::Stream`] → streams chunk-by-chunk via [`StreamReader`];
285    ///   if the stream was already consumed, the reader returns an [`io::Error`]
286    ///   on the first read
287    pub fn into_async_read(self) -> Result<BoxAsyncRead, CamelError> {
288        match self {
289            Body::Empty => Ok(Box::pin(tokio::io::empty())),
290            Body::Bytes(b) => Ok(Box::pin(std::io::Cursor::new(b))),
291            Body::Text(s) => Ok(Box::pin(std::io::Cursor::new(s.into_bytes()))),
292            Body::Json(v) => {
293                let bytes = serde_json::to_vec(&v)
294                    .map_err(|e| CamelError::TypeConversionFailed(e.to_string()))?;
295                Ok(Box::pin(std::io::Cursor::new(bytes)) as BoxAsyncRead)
296            }
297            Body::Xml(s) => Ok(Box::pin(std::io::Cursor::new(s.into_bytes()))),
298            Body::Stream(s) => Ok(Box::pin(StreamAsyncRead {
299                arc: s.stream,
300                reader: None,
301                consumed: false,
302            })),
303        }
304    }
305
306    /// Try to get the body as a string, converting from bytes if needed.
307    pub fn as_text(&self) -> Option<&str> {
308        match self {
309            Body::Text(s) => Some(s.as_str()),
310            _ => None,
311        }
312    }
313
314    /// Try to get the body as an XML string.
315    pub fn as_xml(&self) -> Option<&str> {
316        match self {
317            Body::Xml(s) => Some(s.as_str()),
318            _ => None,
319        }
320    }
321
322    /// Convert this body to `Body::Text`, consuming it.
323    /// Returns `Err(TypeConversionFailed)` if the conversion is not possible.
324    /// `Body::Stream` always fails — materialize with `into_bytes()` first.
325    pub fn try_into_text(self) -> Result<Body, CamelError> {
326        crate::body_converter::convert(self, crate::body_converter::BodyType::Text)
327    }
328
329    /// Convert this body to `Body::Json`, consuming it.
330    /// Returns `Err(TypeConversionFailed)` if the conversion is not possible.
331    /// `Body::Stream` always fails — materialize with `into_bytes()` first.
332    pub fn try_into_json(self) -> Result<Body, CamelError> {
333        crate::body_converter::convert(self, crate::body_converter::BodyType::Json)
334    }
335
336    /// Convert this body to `Body::Bytes`, consuming it.
337    /// Returns `Err(TypeConversionFailed)` if the conversion is not possible.
338    /// `Body::Stream` always fails — materialize with `into_bytes()` first.
339    pub fn try_into_bytes_body(self) -> Result<Body, CamelError> {
340        crate::body_converter::convert(self, crate::body_converter::BodyType::Bytes)
341    }
342
343    /// Convert this body to `Body::Xml`, consuming it.
344    /// Returns `Err(TypeConversionFailed)` if the conversion is not possible.
345    /// `Body::Stream` always fails — materialize with `into_bytes()` first.
346    pub fn try_into_xml(self) -> Result<Body, CamelError> {
347        crate::body_converter::convert(self, crate::body_converter::BodyType::Xml)
348    }
349}
350
351// Conversion impls
352impl From<String> for Body {
353    fn from(s: String) -> Self {
354        Body::Text(s)
355    }
356}
357
358impl From<&str> for Body {
359    fn from(s: &str) -> Self {
360        Body::Text(s.to_string())
361    }
362}
363
364impl From<Bytes> for Body {
365    fn from(b: Bytes) -> Self {
366        Body::Bytes(b)
367    }
368}
369
370impl From<Vec<u8>> for Body {
371    fn from(v: Vec<u8>) -> Self {
372        Body::Bytes(Bytes::from(v))
373    }
374}
375
376impl From<serde_json::Value> for Body {
377    fn from(v: serde_json::Value) -> Self {
378        Body::Json(v)
379    }
380}
381
382#[cfg(test)]
383mod tests {
384    use super::*;
385
386    #[test]
387    fn test_body_default_is_empty() {
388        let body = Body::default();
389        assert!(body.is_empty());
390    }
391
392    #[test]
393    fn test_body_from_string() {
394        let body = Body::from("hello".to_string());
395        assert_eq!(body.as_text(), Some("hello"));
396    }
397
398    #[test]
399    fn test_body_from_str() {
400        let body = Body::from("world");
401        assert_eq!(body.as_text(), Some("world"));
402    }
403
404    #[test]
405    fn test_body_from_bytes() {
406        let body = Body::from(Bytes::from_static(b"data"));
407        assert!(!body.is_empty());
408        assert!(matches!(body, Body::Bytes(_)));
409    }
410
411    #[test]
412    fn test_body_from_json() {
413        let val = serde_json::json!({"key": "value"});
414        let body = Body::from(val.clone());
415        assert!(matches!(body, Body::Json(_)));
416    }
417
418    #[tokio::test]
419    async fn test_into_bytes_from_stream() {
420        use futures::stream;
421        let chunks = vec![Ok(Bytes::from("hello ")), Ok(Bytes::from("world"))];
422        let stream = stream::iter(chunks);
423        let body = Body::Stream(StreamBody {
424            stream: Arc::new(Mutex::new(Some(Box::pin(stream)))),
425            metadata: StreamMetadata::default(),
426        });
427
428        let result = body.into_bytes(100).await.unwrap();
429        assert_eq!(result, Bytes::from("hello world"));
430    }
431
432    #[tokio::test]
433    async fn test_into_bytes_limit_exceeded() {
434        use futures::stream;
435        let chunks = vec![Ok(Bytes::from("this is too long"))];
436        let stream = stream::iter(chunks);
437        let body = Body::Stream(StreamBody {
438            stream: Arc::new(Mutex::new(Some(Box::pin(stream)))),
439            metadata: StreamMetadata::default(),
440        });
441
442        let result = body.into_bytes(5).await;
443        assert!(matches!(result, Err(CamelError::StreamLimitExceeded(5))));
444    }
445
446    #[tokio::test]
447    async fn test_into_bytes_already_consumed() {
448        use futures::stream;
449        let chunks = vec![Ok(Bytes::from("data"))];
450        let stream = stream::iter(chunks);
451        let body = Body::Stream(StreamBody {
452            stream: Arc::new(Mutex::new(Some(Box::pin(stream)))),
453            metadata: StreamMetadata::default(),
454        });
455
456        let cloned = body.clone();
457        let _ = body.into_bytes(100).await.unwrap();
458
459        let result = cloned.into_bytes(100).await;
460        assert!(matches!(result, Err(CamelError::AlreadyConsumed)));
461    }
462
463    #[tokio::test]
464    async fn test_materialize_with_default_limit() {
465        use futures::stream;
466
467        // Small stream under limit - should succeed with default 10MB limit
468        let chunks = vec![Ok(Bytes::from("test data"))];
469        let stream = stream::iter(chunks);
470        let body = Body::Stream(StreamBody {
471            stream: Arc::new(Mutex::new(Some(Box::pin(stream)))),
472            metadata: StreamMetadata::default(),
473        });
474
475        let result = body.materialize().await;
476        assert!(result.is_ok());
477        assert_eq!(result.unwrap(), Bytes::from("test data"));
478    }
479
480    #[tokio::test]
481    async fn test_materialize_non_stream_body_types() {
482        // Verify materialize() works with all body types, not just streams
483
484        // Body::Empty
485        let body = Body::Empty;
486        let result = body.materialize().await.unwrap();
487        assert!(result.is_empty());
488
489        // Body::Bytes
490        let body = Body::Bytes(Bytes::from("bytes data"));
491        let result = body.materialize().await.unwrap();
492        assert_eq!(result, Bytes::from("bytes data"));
493
494        // Body::Text
495        let body = Body::Text("text data".to_string());
496        let result = body.materialize().await.unwrap();
497        assert_eq!(result, Bytes::from("text data"));
498
499        // Body::Json
500        let body = Body::Json(serde_json::json!({"key": "value"}));
501        let result = body.materialize().await.unwrap();
502        assert_eq!(result, Bytes::from_static(br#"{"key":"value"}"#));
503
504        // Body::Xml
505        let xml = "<root><child>value</child></root>";
506        let body = Body::Xml(xml.to_string());
507        let result = body.materialize().await.unwrap();
508        assert_eq!(result, Bytes::from(xml));
509    }
510
511    #[tokio::test]
512    async fn test_materialize_exceeds_default_limit() {
513        use futures::stream;
514
515        // 11MB stream - should fail with default 10MB limit
516        let large_data = vec![0u8; 11 * 1024 * 1024];
517        let chunks = vec![Ok(Bytes::from(large_data))];
518        let stream = stream::iter(chunks);
519        let body = Body::Stream(StreamBody {
520            stream: Arc::new(Mutex::new(Some(Box::pin(stream)))),
521            metadata: StreamMetadata::default(),
522        });
523
524        let result = body.materialize().await;
525        assert!(matches!(
526            result,
527            Err(CamelError::StreamLimitExceeded(10_485_760))
528        ));
529    }
530
531    #[test]
532    fn stream_variants_are_never_equal() {
533        use futures::stream;
534
535        let make_stream = || {
536            let s = stream::iter(vec![Ok(Bytes::from_static(b"data"))]);
537            Body::Stream(StreamBody {
538                stream: Arc::new(Mutex::new(Some(Box::pin(s)))),
539                metadata: StreamMetadata::default(),
540            })
541        };
542        assert_ne!(make_stream(), make_stream());
543    }
544
545    // XML body tests
546
547    #[test]
548    fn test_body_xml_as_xml() {
549        let xml = "<root><child>value</child></root>";
550        let body = Body::Xml(xml.to_string());
551        assert_eq!(body.as_xml(), Some(xml));
552    }
553
554    #[test]
555    fn test_body_non_xml_as_xml_returns_none() {
556        // Body::Text should return None for as_xml()
557        let body = Body::Text("<root/>".to_string());
558        assert_eq!(body.as_xml(), None);
559
560        // Body::Empty should return None
561        let body = Body::Empty;
562        assert_eq!(body.as_xml(), None);
563
564        // Body::Bytes should return None
565        let body = Body::Bytes(Bytes::from("<root/>"));
566        assert_eq!(body.as_xml(), None);
567
568        // Body::Json should return None
569        let body = Body::Json(serde_json::json!({"key": "value"}));
570        assert_eq!(body.as_xml(), None);
571    }
572
573    #[test]
574    fn test_body_xml_partial_eq() {
575        // Same XML content should be equal
576        let body1 = Body::Xml("a".to_string());
577        let body2 = Body::Xml("a".to_string());
578        assert_eq!(body1, body2);
579
580        // Different XML content should not be equal
581        let body1 = Body::Xml("a".to_string());
582        let body2 = Body::Xml("b".to_string());
583        assert_ne!(body1, body2);
584    }
585
586    #[test]
587    fn test_body_xml_not_equal_to_other_variants() {
588        // Body::Xml should not equal Body::Text even with same content
589        let xml_body = Body::Xml("x".to_string());
590        let text_body = Body::Text("x".to_string());
591        assert_ne!(xml_body, text_body);
592    }
593
594    #[test]
595    fn test_try_into_xml_from_text() {
596        let body = Body::Text("<root/>".to_string());
597        let result = body.try_into_xml();
598        assert!(matches!(result, Ok(Body::Xml(ref s)) if s == "<root/>"));
599    }
600
601    #[test]
602    fn test_try_into_xml_invalid_text() {
603        let body = Body::Text("not xml".to_string());
604        let result = body.try_into_xml();
605        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
606    }
607
608    #[test]
609    fn test_body_xml_clone() {
610        let original = Body::Xml("hello".to_string());
611        let cloned = original.clone();
612        assert_eq!(original, cloned);
613    }
614
615    // ---------- into_async_read tests ----------
616
617    #[tokio::test]
618    async fn test_into_async_read_empty() {
619        use tokio::io::AsyncReadExt;
620        let body = Body::Empty;
621        let mut reader = body.into_async_read().unwrap();
622        let mut buf = Vec::new();
623        reader.read_to_end(&mut buf).await.unwrap();
624        assert!(buf.is_empty());
625    }
626
627    #[tokio::test]
628    async fn test_into_async_read_bytes() {
629        use tokio::io::AsyncReadExt;
630        let body = Body::Bytes(Bytes::from("hello"));
631        let mut reader = body.into_async_read().unwrap();
632        let mut buf = Vec::new();
633        reader.read_to_end(&mut buf).await.unwrap();
634        assert_eq!(buf, b"hello");
635    }
636
637    #[tokio::test]
638    async fn test_into_async_read_text() {
639        use tokio::io::AsyncReadExt;
640        let body = Body::Text("world".to_string());
641        let mut reader = body.into_async_read().unwrap();
642        let mut buf = Vec::new();
643        reader.read_to_end(&mut buf).await.unwrap();
644        assert_eq!(buf, b"world");
645    }
646
647    #[tokio::test]
648    async fn test_into_async_read_json() {
649        use tokio::io::AsyncReadExt;
650        let body = Body::Json(serde_json::json!({"key": "val"}));
651        let mut reader = body.into_async_read().unwrap();
652        let mut buf = Vec::new();
653        reader.read_to_end(&mut buf).await.unwrap();
654        let parsed: serde_json::Value = serde_json::from_slice(&buf).unwrap();
655        assert_eq!(parsed["key"], "val");
656    }
657
658    #[tokio::test]
659    async fn test_into_async_read_xml() {
660        use tokio::io::AsyncReadExt;
661        let body = Body::Xml("<root/>".to_string());
662        let mut reader = body.into_async_read().unwrap();
663        let mut buf = Vec::new();
664        reader.read_to_end(&mut buf).await.unwrap();
665        assert_eq!(buf, b"<root/>");
666    }
667
668    #[tokio::test]
669    async fn test_into_async_read_stream_multichunk() {
670        use tokio::io::AsyncReadExt;
671        let chunks: Vec<Result<Bytes, CamelError>> = vec![
672            Ok(Bytes::from("foo")),
673            Ok(Bytes::from("bar")),
674            Ok(Bytes::from("baz")),
675        ];
676        let stream = futures::stream::iter(chunks);
677        let body = Body::Stream(StreamBody {
678            stream: Arc::new(Mutex::new(Some(Box::pin(stream)))),
679            metadata: StreamMetadata {
680                size_hint: None,
681                content_type: None,
682                origin: None,
683            },
684        });
685        let mut reader = body.into_async_read().unwrap();
686        let mut buf = Vec::new();
687        reader.read_to_end(&mut buf).await.unwrap();
688        assert_eq!(buf, b"foobarbaz");
689    }
690
691    #[tokio::test]
692    async fn test_into_async_read_already_consumed() {
693        use tokio::io::AsyncReadExt;
694        // Mutex holds None → stream already consumed
695        type MaybeStream = Arc<Mutex<Option<BoxStream<'static, Result<Bytes, CamelError>>>>>;
696        let arc: MaybeStream = Arc::new(Mutex::new(None));
697        let body = Body::Stream(StreamBody {
698            stream: arc,
699            metadata: StreamMetadata {
700                size_hint: None,
701                content_type: None,
702                origin: None,
703            },
704        });
705        let mut reader = body.into_async_read().unwrap();
706        let mut buf = Vec::new();
707        let result = reader.read_to_end(&mut buf).await;
708        assert!(result.is_err());
709    }
710}