Skip to main content

rama_http_types/body/
capture.rs

1use parking_lot::Mutex;
2use pin_project_lite::pin_project;
3use rama_core::bytes::{Bytes, BytesMut};
4use std::{
5    fmt,
6    future::{Future, ready},
7    pin::Pin,
8    task::{Context, Poll, ready},
9};
10use sync_wrapper::SyncWrapper;
11use tokio::sync::oneshot;
12
13use super::{Body, Frame, SizeHint, StreamingBody};
14use crate::HeaderMap;
15
16/// How an observed body stream terminated.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub enum CaptureOutcome {
19    /// The body returned its normal end-of-stream marker.
20    Complete,
21    /// The body yielded an error.
22    Error,
23    /// The wrapper was dropped before observing a normal end or error.
24    ///
25    /// This does not necessarily imply missing bytes: a consumer can stop
26    /// polling after receiving all expected data without polling the terminal
27    /// `None`. A custom sink can also record a frame before its future finishes,
28    /// even if cancellation prevents that frame from reaching the downstream
29    /// consumer.
30    Aborted,
31}
32
33/// An owned event emitted while a body streams.
34///
35/// Data frames contain a reference-counted clone of the original [`Bytes`].
36/// The original frame continues downstream unchanged after the capture sink's
37/// future completes.
38#[derive(Clone, Debug)]
39pub enum BodyCaptureEvent {
40    /// A data or trailers frame.
41    Frame(Frame<Bytes>),
42    /// The capture reached a normal end or observed an error.
43    ///
44    /// Error outcomes intentionally carry no error value so events remain
45    /// cloneable without constraining the wrapped body's error type.
46    End(CaptureOutcome),
47}
48
49/// Asynchronously observes owned body events.
50///
51/// The future returned by [`capture`](Self::capture) is polled before the
52/// original frame or terminal state is forwarded. Implementations therefore
53/// choose their own flow-control policy: they can perform work directly, await
54/// a bounded queue, enqueue without waiting, or return immediately.
55pub trait BodyCaptureSink: Send + Sync + 'static {
56    /// Capture one body event.
57    ///
58    /// The returned future must own everything it needs because it can outlive
59    /// the borrow of `self`.
60    fn capture(&self, event: BodyCaptureEvent) -> impl Future<Output = ()> + Send + 'static;
61
62    /// Notify the sink synchronously when the body is abandoned.
63    ///
64    /// Destructors cannot await. Implementations that need asynchronous work
65    /// can perform a non-blocking send or arrange for their own guard to finish
66    /// that work.
67    fn aborted(&self) {}
68}
69
70impl<F, Fut> BodyCaptureSink for F
71where
72    F: Fn(BodyCaptureEvent) -> Fut + Send + Sync + 'static,
73    Fut: Future<Output = ()> + Send + 'static,
74{
75    fn capture(&self, event: BodyCaptureEvent) -> impl Future<Output = ()> + Send + 'static {
76        self(event)
77    }
78}
79
80impl BodyCaptureSink for tokio::sync::mpsc::Sender<BodyCaptureEvent> {
81    fn capture(&self, event: BodyCaptureEvent) -> impl Future<Output = ()> + Send + 'static {
82        let sender = self.clone();
83        async move {
84            drop(sender.send(event).await);
85        }
86    }
87
88    fn aborted(&self) {
89        // Drop cannot wait for bounded capacity. Consumers that require a
90        // guaranteed abort outcome should provide a sink with its own guard.
91        drop(self.try_send(BodyCaptureEvent::End(CaptureOutcome::Aborted)));
92    }
93}
94
95impl BodyCaptureSink for tokio::sync::mpsc::UnboundedSender<BodyCaptureEvent> {
96    fn capture(&self, event: BodyCaptureEvent) -> impl Future<Output = ()> + Send + 'static {
97        let result = self.send(event);
98        async move {
99            drop(result);
100        }
101    }
102
103    fn aborted(&self) {
104        drop(self.send(BodyCaptureEvent::End(CaptureOutcome::Aborted)));
105    }
106}
107
108type CaptureFuture = Pin<Box<dyn Future<Output = ()> + Send + 'static>>;
109
110enum PendingOutput<E> {
111    Frame(Frame<Bytes>),
112    Error(E),
113    End,
114}
115
116struct CaptureGuard<S: BodyCaptureSink> {
117    sink: S,
118    finished: bool,
119}
120
121impl<S: BodyCaptureSink> CaptureGuard<S> {
122    fn finish(&mut self) {
123        self.finished = true;
124    }
125}
126
127impl<S: BodyCaptureSink> Drop for CaptureGuard<S> {
128    fn drop(&mut self) {
129        if !self.finished {
130            self.sink.aborted();
131        }
132    }
133}
134
135pin_project! {
136    /// A body that asynchronously sends an owned copy of each frame to a sink.
137    ///
138    /// At most one original frame and its reference-counted capture copy are
139    /// retained while the sink future is pending. No whole-body buffering or
140    /// internal channel is used.
141    #[must_use = "a captured body does nothing unless polled"]
142    pub struct CaptureBody<B, S>
143    where
144        B: StreamingBody<Data = Bytes>,
145        S: BodyCaptureSink,
146    {
147        #[pin]
148        inner: B,
149        guard: CaptureGuard<S>,
150        pending: SyncWrapper<Option<CaptureFuture>>,
151        pending_output: Option<PendingOutput<B::Error>>,
152    }
153}
154
155impl<B, S> CaptureBody<B, S>
156where
157    B: StreamingBody<Data = Bytes>,
158    S: BodyCaptureSink,
159{
160    /// Wrap a body with an asynchronous capture sink.
161    pub fn new(inner: B, sink: S) -> Self {
162        Self {
163            inner,
164            guard: CaptureGuard {
165                sink,
166                finished: false,
167            },
168            pending: SyncWrapper::new(None),
169            pending_output: None,
170        }
171    }
172
173    /// Return a reference to the wrapped body.
174    pub const fn get_ref(&self) -> &B {
175        &self.inner
176    }
177
178    /// Return a mutable reference to the wrapped body.
179    pub fn get_mut(&mut self) -> &mut B {
180        &mut self.inner
181    }
182
183    /// Return a pinned mutable reference to the wrapped body.
184    pub fn get_pin_mut(self: Pin<&mut Self>) -> Pin<&mut B> {
185        self.project().inner
186    }
187
188    /// Consume the wrapper and return the inner body.
189    ///
190    /// The sink receives [`BodyCaptureSink::aborted`].
191    pub fn into_inner(self) -> B {
192        self.inner
193    }
194}
195
196impl<B> CaptureBody<B, BufferedBodyCapture>
197where
198    B: StreamingBody<Data = Bytes>,
199{
200    /// Wrap a body and retain a bounded or unlimited copy in memory.
201    pub fn buffered(inner: B, limit: CaptureLimit) -> (Self, CaptureHandle) {
202        let (sink, handle) = BufferedBodyCapture::new(limit);
203        (Self::new(inner, sink), handle)
204    }
205}
206
207impl<B, S> StreamingBody for CaptureBody<B, S>
208where
209    B: StreamingBody<Data = Bytes>,
210    S: BodyCaptureSink,
211{
212    type Data = Bytes;
213    type Error = B::Error;
214
215    fn poll_frame(
216        self: Pin<&mut Self>,
217        cx: &mut Context<'_>,
218    ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
219        let mut this = self.project();
220
221        loop {
222            if let Some(future) = this.pending.get_mut().as_mut() {
223                ready!(future.as_mut().poll(cx));
224                *this.pending.get_mut() = None;
225
226                #[expect(
227                    clippy::expect_used,
228                    reason = "missing output means the CaptureBody state machine is invalid"
229                )]
230                let output = this
231                    .pending_output
232                    .take()
233                    .expect("a capture future always has a pending output");
234                match output {
235                    PendingOutput::Frame(frame) => return Poll::Ready(Some(Ok(frame))),
236                    PendingOutput::Error(error) => {
237                        this.guard.finish();
238                        return Poll::Ready(Some(Err(error)));
239                    }
240                    PendingOutput::End => {
241                        this.guard.finish();
242                        return Poll::Ready(None);
243                    }
244                }
245            }
246
247            if this.guard.finished {
248                return this.inner.poll_frame(cx);
249            }
250
251            match this.inner.as_mut().poll_frame(cx) {
252                Poll::Pending => return Poll::Pending,
253                Poll::Ready(Some(Ok(frame))) => {
254                    let future = this
255                        .guard
256                        .sink
257                        .capture(BodyCaptureEvent::Frame(frame.clone()));
258                    *this.pending.get_mut() = Some(Box::pin(future));
259                    *this.pending_output = Some(PendingOutput::Frame(frame));
260                }
261                Poll::Ready(Some(Err(error))) => {
262                    let future = this
263                        .guard
264                        .sink
265                        .capture(BodyCaptureEvent::End(CaptureOutcome::Error));
266                    *this.pending.get_mut() = Some(Box::pin(future));
267                    *this.pending_output = Some(PendingOutput::Error(error));
268                }
269                Poll::Ready(None) => {
270                    let future = this
271                        .guard
272                        .sink
273                        .capture(BodyCaptureEvent::End(CaptureOutcome::Complete));
274                    *this.pending.get_mut() = Some(Box::pin(future));
275                    *this.pending_output = Some(PendingOutput::End);
276                }
277            }
278        }
279    }
280
281    fn is_end_stream(&self) -> bool {
282        self.guard.finished && self.inner.is_end_stream()
283    }
284
285    fn size_hint(&self) -> SizeHint {
286        let pending = match &self.pending_output {
287            Some(PendingOutput::Frame(frame)) => frame
288                .data_ref()
289                .map_or(0, |data| u64::try_from(data.len()).unwrap_or(u64::MAX)),
290            _ => 0,
291        };
292        self.inner.size_hint() + SizeHint::with_exact(pending)
293    }
294}
295
296impl<B, S> fmt::Debug for CaptureBody<B, S>
297where
298    B: StreamingBody<Data = Bytes> + fmt::Debug,
299    S: BodyCaptureSink,
300{
301    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
302        f.debug_struct("CaptureBody")
303            .field("inner", &self.inner)
304            .field("sink", &std::any::type_name::<S>())
305            .field("capture_pending", &self.pending_output.is_some())
306            .field("finished", &self.guard.finished)
307            .finish()
308    }
309}
310
311/// Maximum number of body bytes retained by a [`BufferedBodyCapture`].
312#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
313pub struct CaptureLimit(Option<usize>);
314
315impl CaptureLimit {
316    /// Retain at most `max_bytes`.
317    #[must_use]
318    pub const fn max_bytes(max_bytes: usize) -> Self {
319        Self(Some(max_bytes))
320    }
321
322    /// Retain the complete body without a size bound.
323    #[must_use]
324    pub const fn unlimited() -> Self {
325        Self(None)
326    }
327
328    /// Return the configured byte limit, or `None` for an unlimited capture.
329    #[must_use]
330    pub const fn get(self) -> Option<usize> {
331        self.0
332    }
333}
334
335impl From<usize> for CaptureLimit {
336    fn from(value: usize) -> Self {
337        Self::max_bytes(value)
338    }
339}
340
341/// The final output produced by [`BufferedBodyCapture`].
342#[derive(Debug)]
343pub struct CapturedBody {
344    bytes: Bytes,
345    trailers: Option<HeaderMap>,
346    outcome: CaptureOutcome,
347    total_bytes: u64,
348    truncated: bool,
349}
350
351impl CapturedBody {
352    /// Return the retained body bytes.
353    #[must_use]
354    pub const fn bytes(&self) -> &Bytes {
355        &self.bytes
356    }
357
358    /// Return all observed trailer fields.
359    ///
360    /// The byte limit applies only to data frames; trailer storage is not
361    /// bounded. Fields from multiple trailer frames are merged in observation
362    /// order using [`HeaderMap::extend`].
363    #[must_use]
364    pub const fn trailers(&self) -> Option<&HeaderMap> {
365        self.trailers.as_ref()
366    }
367
368    /// Return how the stream terminated.
369    #[must_use]
370    pub const fn outcome(&self) -> CaptureOutcome {
371        self.outcome
372    }
373
374    /// Return the total number of data bytes observed.
375    #[must_use]
376    pub const fn total_bytes(&self) -> u64 {
377        self.total_bytes
378    }
379
380    /// Return whether bytes were omitted because the capture limit was hit.
381    #[must_use]
382    pub const fn is_truncated(&self) -> bool {
383        self.truncated
384    }
385
386    /// Consume the capture and return its retained bytes and trailers.
387    #[must_use]
388    pub fn into_parts(self) -> (Bytes, Option<HeaderMap>) {
389        (self.bytes, self.trailers)
390    }
391
392    /// Rebuild a forwardable body from the retained bytes and trailers.
393    pub fn into_body(self) -> Body {
394        let (bytes, trailers) = self.into_parts();
395        match trailers {
396            Some(trailers) => Body::from(bytes).with_trailer_headers(trailers),
397            None => Body::from(bytes),
398        }
399    }
400}
401
402/// Returned if a buffered capture producer disappears without finalizing.
403#[derive(Debug, Clone, Copy, PartialEq, Eq)]
404pub struct CaptureCanceled;
405
406impl fmt::Display for CaptureCanceled {
407    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
408        f.write_str("body capture producer was canceled")
409    }
410}
411
412impl std::error::Error for CaptureCanceled {}
413
414/// An exactly-once completion handle for a [`BufferedBodyCapture`].
415#[derive(Debug)]
416#[must_use = "drop the handle explicitly if the buffered body is not needed"]
417pub struct CaptureHandle {
418    receiver: oneshot::Receiver<CapturedBody>,
419}
420
421impl CaptureHandle {
422    /// Wait until the body completes, errors, or is dropped early.
423    pub async fn wait(self) -> Result<CapturedBody, CaptureCanceled> {
424        self.receiver.await.map_err(|_error| CaptureCanceled)
425    }
426}
427
428struct BufferState {
429    bytes: BytesMut,
430    trailers: Option<HeaderMap>,
431    limit: CaptureLimit,
432    total_bytes: u64,
433    truncated: bool,
434    sender: Option<oneshot::Sender<CapturedBody>>,
435}
436
437impl BufferState {
438    fn is_observed(&mut self) -> bool {
439        if self.sender.as_ref().is_some_and(oneshot::Sender::is_closed) {
440            self.sender = None;
441            self.bytes.clear();
442            self.trailers = None;
443        }
444        self.sender.is_some()
445    }
446
447    fn observe_frame(&mut self, frame: Frame<Bytes>) {
448        if !self.is_observed() {
449            return;
450        }
451
452        match frame.into_data() {
453            Ok(data) => {
454                self.total_bytes = self
455                    .total_bytes
456                    .saturating_add(u64::try_from(data.len()).unwrap_or(u64::MAX));
457                let remaining = self
458                    .limit
459                    .get()
460                    .map_or(data.len(), |limit| limit.saturating_sub(self.bytes.len()));
461                let take = remaining.min(data.len());
462                self.bytes.extend_from_slice(&data[..take]);
463                self.truncated |= take < data.len();
464            }
465            Err(frame) => {
466                if let Ok(trailers) = frame.into_trailers() {
467                    if let Some(current) = &mut self.trailers {
468                        current.extend(trailers);
469                    } else {
470                        self.trailers = Some(trailers);
471                    }
472                }
473            }
474        }
475    }
476
477    fn finish(&mut self, outcome: CaptureOutcome) {
478        let Some(sender) = self.sender.take() else {
479            return;
480        };
481        let captured = CapturedBody {
482            bytes: std::mem::take(&mut self.bytes).freeze(),
483            trailers: self.trailers.take(),
484            outcome,
485            total_bytes: self.total_bytes,
486            truncated: self.truncated,
487        };
488        drop(sender.send(captured));
489    }
490}
491
492impl Drop for BufferState {
493    fn drop(&mut self) {
494        self.finish(CaptureOutcome::Aborted);
495    }
496}
497
498/// A capture sink that retains body data and trailers in memory.
499///
500/// The associated [`CaptureHandle`] resolves exactly once with the buffered
501/// body and its terminal outcome. This is an explicit collection utility on
502/// top of streaming [`CaptureBody`], rather than part of the forwarding path.
503pub struct BufferedBodyCapture {
504    state: Mutex<BufferState>,
505}
506
507impl BufferedBodyCapture {
508    /// Create a buffered sink and its exactly-once completion handle.
509    pub fn new(limit: CaptureLimit) -> (Self, CaptureHandle) {
510        let (sender, receiver) = oneshot::channel();
511        (
512            Self {
513                state: Mutex::new(BufferState {
514                    bytes: BytesMut::new(),
515                    trailers: None,
516                    limit,
517                    total_bytes: 0,
518                    truncated: false,
519                    sender: Some(sender),
520                }),
521            },
522            CaptureHandle { receiver },
523        )
524    }
525
526    fn state(&self) -> parking_lot::MutexGuard<'_, BufferState> {
527        self.state.lock()
528    }
529}
530
531impl BodyCaptureSink for BufferedBodyCapture {
532    fn capture(&self, event: BodyCaptureEvent) -> impl Future<Output = ()> + Send + 'static {
533        let mut state = self.state();
534        match event {
535            BodyCaptureEvent::Frame(frame) => state.observe_frame(frame),
536            BodyCaptureEvent::End(outcome) => state.finish(outcome),
537        }
538        ready(())
539    }
540
541    fn aborted(&self) {
542        self.state().finish(CaptureOutcome::Aborted);
543    }
544}
545
546impl fmt::Debug for BufferedBodyCapture {
547    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
548        let state = self.state();
549        f.debug_struct("BufferedBodyCapture")
550            .field("limit", &state.limit)
551            .field("captured_bytes", &state.bytes.len())
552            .field("total_bytes", &state.total_bytes)
553            .field("truncated", &state.truncated)
554            .finish_non_exhaustive()
555    }
556}
557
558#[cfg(test)]
559mod tests {
560    use std::{
561        convert::Infallible,
562        io,
563        sync::{
564            Arc,
565            atomic::{AtomicUsize, Ordering},
566        },
567    };
568
569    use rama_core::bytes::Bytes;
570
571    use super::*;
572    use crate::body::util::{BodyExt as _, StreamBody};
573
574    async fn wait_for_capture(handle: CaptureHandle) -> CapturedBody {
575        tokio::time::timeout(std::time::Duration::from_secs(1), handle.wait())
576            .await
577            .expect("capture should finish promptly")
578            .expect("capture producer should finalize")
579    }
580
581    #[tokio::test]
582    async fn sink_future_applies_backpressure_before_forwarding() {
583        let gate = Arc::new(tokio::sync::Notify::new());
584        let sink_gate = Arc::clone(&gate);
585        let sink = move |event: BodyCaptureEvent| {
586            let gate = Arc::clone(&sink_gate);
587            async move {
588                if matches!(event, BodyCaptureEvent::Frame(_)) {
589                    gate.notified().await;
590                }
591            }
592        };
593        let mut body = CaptureBody::new(Body::from("held"), sink);
594        let mut frame = Box::pin(body.frame());
595
596        tokio::time::timeout(std::time::Duration::ZERO, &mut frame)
597            .await
598            .expect_err("the frame must wait for its capture future");
599        drop(frame);
600        assert_eq!(body.size_hint().exact(), Some(4));
601        assert!(!body.is_end_stream());
602        gate.notify_one();
603        assert_eq!(
604            body.frame().await.unwrap().unwrap().into_data().unwrap(),
605            Bytes::from_static(b"held")
606        );
607    }
608
609    #[tokio::test]
610    async fn channel_sinks_report_abort_when_capacity_is_available() {
611        let (bounded, mut bounded_events) = tokio::sync::mpsc::channel(1);
612        bounded.aborted();
613        assert!(matches!(
614            tokio::time::timeout(std::time::Duration::from_secs(1), bounded_events.recv())
615                .await
616                .expect("bounded abort should arrive promptly"),
617            Some(BodyCaptureEvent::End(CaptureOutcome::Aborted))
618        ));
619
620        let (unbounded, mut unbounded_events) = tokio::sync::mpsc::unbounded_channel();
621        unbounded.aborted();
622        assert!(matches!(
623            tokio::time::timeout(std::time::Duration::from_secs(1), unbounded_events.recv())
624                .await
625                .expect("unbounded abort should arrive promptly"),
626            Some(BodyCaptureEvent::End(CaptureOutcome::Aborted))
627        ));
628    }
629
630    #[tokio::test]
631    async fn bounded_channel_drops_abort_notification_when_full() {
632        let (bounded, mut events) = tokio::sync::mpsc::channel(1);
633        bounded
634            .send(BodyCaptureEvent::Frame(Frame::data(Bytes::from_static(
635                b"queued",
636            ))))
637            .await
638            .unwrap();
639
640        bounded.aborted();
641        drop(bounded);
642
643        assert!(matches!(
644            events.recv().await,
645            Some(BodyCaptureEvent::Frame(_))
646        ));
647        assert!(events.recv().await.is_none());
648    }
649
650    #[tokio::test]
651    async fn buffered_capture_forwards_frames_and_finishes_once() {
652        let mut trailers = HeaderMap::new();
653        trailers.insert("x-finished", "yes".parse().unwrap());
654        let source = StreamBody::new(rama_core::futures::stream::iter([
655            Ok::<_, Infallible>(Frame::data(Bytes::from_static(b"hello"))),
656            Ok(Frame::data(Bytes::from_static(b" world"))),
657            Ok(Frame::trailers(trailers.clone())),
658        ]));
659        let (mut body, handle) = CaptureBody::buffered(source, CaptureLimit::max_bytes(7));
660
661        let mut forwarded = Vec::new();
662        while let Some(frame) = body.frame().await {
663            let frame = frame.unwrap();
664            if let Some(data) = frame.data_ref() {
665                forwarded.extend_from_slice(data);
666            } else {
667                assert_eq!(frame.trailers_ref().unwrap(), &trailers);
668            }
669        }
670
671        let captured = wait_for_capture(handle).await;
672        assert_eq!(forwarded, b"hello world");
673        assert_eq!(captured.bytes(), &Bytes::from_static(b"hello w"));
674        assert_eq!(captured.trailers(), Some(&trailers));
675        assert_eq!(captured.outcome(), CaptureOutcome::Complete);
676        assert_eq!(captured.total_bytes(), 11);
677        assert!(captured.is_truncated());
678    }
679
680    #[tokio::test]
681    async fn zero_byte_limit_records_length_without_retaining_data() {
682        let (body, handle) =
683            CaptureBody::buffered(Body::from("not retained"), CaptureLimit::max_bytes(0));
684        body.collect().await.unwrap();
685
686        let captured = wait_for_capture(handle).await;
687        assert!(captured.bytes().is_empty());
688        assert_eq!(captured.total_bytes(), 12);
689        assert!(captured.is_truncated());
690        assert_eq!(captured.outcome(), CaptureOutcome::Complete);
691    }
692
693    #[tokio::test]
694    async fn buffered_capture_reports_error_and_partial_body() {
695        let source = StreamBody::new(rama_core::futures::stream::iter([
696            Ok(Frame::data(Bytes::from_static(b"before"))),
697            Err(io::Error::other("broken body")),
698        ]));
699        let (mut body, handle) = CaptureBody::buffered(source, CaptureLimit::unlimited());
700
701        assert_eq!(
702            body.frame().await.unwrap().unwrap().into_data().unwrap(),
703            Bytes::from_static(b"before")
704        );
705        assert_eq!(
706            body.frame().await.unwrap().unwrap_err().to_string(),
707            "broken body"
708        );
709
710        let captured = wait_for_capture(handle).await;
711        assert_eq!(captured.bytes(), &Bytes::from_static(b"before"));
712        assert_eq!(captured.outcome(), CaptureOutcome::Error);
713    }
714
715    #[tokio::test]
716    async fn unlimited_buffer_can_rebuild_data_and_trailers() {
717        let mut trailers = HeaderMap::new();
718        trailers.insert("x-finished", "yes".parse().unwrap());
719        let source = StreamBody::new(rama_core::futures::stream::iter([
720            Ok::<_, Infallible>(Frame::data(Bytes::from_static(b"complete"))),
721            Ok(Frame::trailers(trailers.clone())),
722        ]));
723        let (body, handle) = CaptureBody::buffered(source, CaptureLimit::unlimited());
724        body.collect().await.unwrap();
725
726        let captured = wait_for_capture(handle).await;
727        assert_eq!(captured.outcome(), CaptureOutcome::Complete);
728        assert!(!captured.is_truncated());
729        let rebuilt = captured.into_body().collect().await.unwrap();
730        assert_eq!(rebuilt.trailers(), Some(&trailers));
731        assert_eq!(rebuilt.to_bytes(), Bytes::from_static(b"complete"));
732    }
733
734    #[tokio::test]
735    async fn dropping_buffer_handle_stops_retaining_frames() {
736        let (sink, handle) = BufferedBodyCapture::new(CaptureLimit::unlimited());
737        drop(handle);
738        sink.capture(BodyCaptureEvent::Frame(Frame::data(Bytes::from_static(
739            b"ignored",
740        ))))
741        .await;
742
743        let state = sink.state();
744        assert!(state.sender.is_none());
745        assert!(state.bytes.is_empty());
746        assert_eq!(state.total_bytes, 0);
747    }
748
749    #[tokio::test]
750    async fn dropping_buffer_sink_reports_abort() {
751        let (sink, handle) = BufferedBodyCapture::new(CaptureLimit::unlimited());
752        drop(sink);
753
754        let captured = wait_for_capture(handle).await;
755        assert_eq!(captured.outcome(), CaptureOutcome::Aborted);
756    }
757
758    #[tokio::test]
759    async fn explicit_buffer_abort_finishes_before_the_sink_is_dropped() {
760        let (sink, handle) = BufferedBodyCapture::new(CaptureLimit::unlimited());
761        sink.aborted();
762
763        let captured = wait_for_capture(handle).await;
764        assert_eq!(captured.outcome(), CaptureOutcome::Aborted);
765        drop(sink);
766    }
767
768    #[tokio::test]
769    async fn buffered_capture_reports_abort_once() {
770        let source = StreamBody::new(rama_core::futures::stream::iter([
771            Ok::<_, Infallible>(Frame::data(Bytes::from_static(b"first"))),
772            Ok(Frame::data(Bytes::from_static(b"second"))),
773        ]));
774        let (mut body, handle) = CaptureBody::buffered(source, CaptureLimit::unlimited());
775        assert_eq!(
776            body.frame().await.unwrap().unwrap().into_data().unwrap(),
777            Bytes::from_static(b"first")
778        );
779        drop(body);
780
781        let captured = wait_for_capture(handle).await;
782        assert_eq!(captured.bytes(), &Bytes::from_static(b"first"));
783        assert_eq!(captured.outcome(), CaptureOutcome::Aborted);
784    }
785
786    struct PendingSink {
787        events: tokio::sync::mpsc::UnboundedSender<BodyCaptureEvent>,
788        aborts: Arc<AtomicUsize>,
789    }
790
791    impl BodyCaptureSink for PendingSink {
792        fn capture(&self, event: BodyCaptureEvent) -> impl Future<Output = ()> + Send + 'static {
793            self.events.send(event).unwrap();
794            std::future::pending()
795        }
796
797        fn aborted(&self) {
798            self.aborts.fetch_add(1, Ordering::Relaxed);
799        }
800    }
801
802    #[tokio::test]
803    async fn dropping_while_sink_is_pending_reports_abort_without_forwarding() {
804        let (events, mut captured) = tokio::sync::mpsc::unbounded_channel();
805        let aborts = Arc::new(AtomicUsize::new(0));
806        let mut body = CaptureBody::new(
807            Body::from("captured first"),
808            PendingSink {
809                events,
810                aborts: Arc::clone(&aborts),
811            },
812        );
813        let mut frame = Box::pin(body.frame());
814
815        tokio::time::timeout(std::time::Duration::ZERO, &mut frame)
816            .await
817            .expect_err("the sink future should remain pending");
818        drop(frame);
819        assert!(matches!(
820            captured.recv().await,
821            Some(BodyCaptureEvent::Frame(_))
822        ));
823        drop(body);
824        assert_eq!(aborts.load(Ordering::Relaxed), 1);
825    }
826
827    struct AbortSink(Arc<AtomicUsize>);
828
829    impl BodyCaptureSink for AbortSink {
830        fn capture(&self, _event: BodyCaptureEvent) -> impl Future<Output = ()> + Send + 'static {
831            ready(())
832        }
833
834        fn aborted(&self) {
835            self.0.fetch_add(1, Ordering::Relaxed);
836        }
837    }
838
839    #[test]
840    fn into_inner_notifies_sink_of_abort() {
841        let aborted = Arc::new(AtomicUsize::new(0));
842        let body = CaptureBody::new(Body::from("body"), AbortSink(Arc::clone(&aborted)));
843        let _inner = body.into_inner();
844        assert_eq!(aborted.load(Ordering::Relaxed), 1);
845    }
846
847    struct CompletionSink {
848        endings: Arc<AtomicUsize>,
849        aborts: Arc<AtomicUsize>,
850    }
851
852    impl BodyCaptureSink for CompletionSink {
853        fn capture(&self, event: BodyCaptureEvent) -> impl Future<Output = ()> + Send + 'static {
854            if matches!(event, BodyCaptureEvent::End(CaptureOutcome::Complete)) {
855                self.endings.fetch_add(1, Ordering::Relaxed);
856            }
857            ready(())
858        }
859
860        fn aborted(&self) {
861            self.aborts.fetch_add(1, Ordering::Relaxed);
862        }
863    }
864
865    #[tokio::test]
866    async fn normal_completion_finishes_once_without_abort() {
867        let endings = Arc::new(AtomicUsize::new(0));
868        let aborts = Arc::new(AtomicUsize::new(0));
869        let mut body = CaptureBody::new(
870            Body::empty(),
871            CompletionSink {
872                endings: Arc::clone(&endings),
873                aborts: Arc::clone(&aborts),
874            },
875        );
876
877        assert!(body.frame().await.is_none());
878        assert!(body.is_end_stream());
879        drop(body);
880
881        assert_eq!(endings.load(Ordering::Relaxed), 1);
882        assert_eq!(aborts.load(Ordering::Relaxed), 0);
883    }
884
885    #[test]
886    fn delegates_stream_metadata_without_hiding_pending_completion() {
887        let (body, handle) = CaptureBody::buffered(Body::from("abc"), CaptureLimit::unlimited());
888        assert!(!body.is_end_stream());
889        assert_eq!(body.size_hint().exact(), Some(3));
890        drop(handle);
891
892        let (body, handle) = CaptureBody::buffered(Body::empty(), CaptureLimit::unlimited());
893        assert!(!body.is_end_stream());
894        assert_eq!(body.size_hint().exact(), Some(0));
895        drop(handle);
896    }
897
898    #[test]
899    fn capture_debug_and_canceled_display_are_informative() {
900        let (sink, handle) = BufferedBodyCapture::new(CaptureLimit::max_bytes(8));
901        assert!(format!("{sink:?}").contains("BufferedBodyCapture"));
902        let body = CaptureBody::new(Body::empty(), sink);
903        assert!(format!("{body:?}").contains("CaptureBody"));
904        assert_eq!(
905            CaptureCanceled.to_string(),
906            "body capture producer was canceled"
907        );
908        drop(handle);
909    }
910}