Skip to main content

rama_http/layer/timeout/
deadline_body.rs

1use super::TimeoutError;
2use crate::body::{Frame, SizeHint, StreamingBody};
3use pin_project_lite::pin_project;
4use rama_core::error::{BoxError, ErrorContext as _};
5use std::{
6    pin::Pin,
7    task::{Context, Poll, ready},
8    time::Duration,
9};
10use tokio::time::{Sleep, sleep};
11
12pin_project! {
13    /// Wrapper around a [`Body`][`http_body::Body`] that enforces a hard deadline on the entire body transfer.
14    ///
15    /// Unlike [`TimeoutBody`][super::TimeoutBody], which resets its deadline each time a frame is
16    /// received, `DeadlineBody` starts a single timer at construction and returns a
17    /// [`TimeoutError`] if the body is not fully consumed before the deadline.
18    ///
19    /// The deadline is **wall-clock time from construction**, not cumulative poll time. The
20    /// timer continues to count even if the consumer is not actively polling the body. If you
21    /// poll some frames, pause to do other work, and then resume, the elapsed pause time counts
22    /// toward the deadline.
23    ///
24    /// # When to use this
25    ///
26    /// This is primarily useful as middleware on public-facing endpoints where you want to bound
27    /// the total wall-clock time a single request can hold resources (task slots, memory for
28    /// buffering, etc.), regardless of how frequently data trickles in. A slow client sending
29    /// one byte per second will never trip [`TimeoutBody`][super::TimeoutBody]'s idle timeout,
30    /// but will correctly trip `DeadlineBody`.
31    ///
32    /// If you only need to detect stalled connections where no data flows for a period, use
33    /// [`TimeoutBody`][super::TimeoutBody] instead. The two can be stacked if you want both
34    /// an idle timeout and a hard deadline.
35    pub struct DeadlineBody<B> {
36        #[pin]
37        sleep: Sleep,
38        #[pin]
39        body: B,
40    }
41}
42
43impl<B> DeadlineBody<B> {
44    /// Creates a new [`DeadlineBody`].
45    ///
46    /// The timeout starts immediately. If the body is not fully consumed within `timeout`,
47    /// subsequent `poll_frame` calls will return a [`TimeoutError`].
48    pub fn new(timeout: Duration, body: B) -> Self {
49        Self {
50            sleep: sleep(timeout),
51            body,
52        }
53    }
54}
55
56impl<B> StreamingBody for DeadlineBody<B>
57where
58    B: StreamingBody,
59    B::Error: Into<BoxError>,
60{
61    type Data = B::Data;
62    type Error = BoxError;
63
64    fn poll_frame(
65        self: Pin<&mut Self>,
66        cx: &mut Context<'_>,
67    ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
68        let this = self.project();
69
70        // Error if the absolute deadline has expired.
71        if this.sleep.poll(cx) == Poll::Ready(()) {
72            return Poll::Ready(Some(Err(Box::new(TimeoutError(())))));
73        }
74
75        // Check for body data.
76        let frame = ready!(this.body.poll_frame(cx));
77
78        Poll::Ready(frame.transpose().into_box_error().transpose())
79    }
80
81    fn is_end_stream(&self) -> bool {
82        self.body.is_end_stream()
83    }
84
85    fn size_hint(&self) -> SizeHint {
86        self.body.size_hint()
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    use crate::body::util::BodyExt;
95    use pin_project_lite::pin_project;
96    use rama_core::bytes::Bytes;
97    use std::{error::Error, fmt::Display};
98
99    #[derive(Debug)]
100    struct MockError;
101
102    impl Error for MockError {}
103
104    impl Display for MockError {
105        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106            write!(f, "mock error")
107        }
108    }
109
110    pin_project! {
111        /// A body that yields a single frame after a delay.
112        struct MockBody {
113            #[pin]
114            sleep: Sleep,
115        }
116    }
117
118    impl StreamingBody for MockBody {
119        type Data = Bytes;
120        type Error = MockError;
121
122        fn poll_frame(
123            self: Pin<&mut Self>,
124            cx: &mut Context<'_>,
125        ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
126            let this = self.project();
127            this.sleep
128                .poll(cx)
129                .map(|()| Some(Ok(Frame::data(vec![].into()))))
130        }
131    }
132
133    pin_project! {
134        /// A body that yields multiple frames with a delay between each.
135        struct MultiFrameBody {
136            frames_remaining: usize,
137            frame_interval: Duration,
138            #[pin]
139            sleep: Option<Sleep>,
140        }
141    }
142
143    impl StreamingBody for MultiFrameBody {
144        type Data = Bytes;
145        type Error = MockError;
146
147        fn poll_frame(
148            self: Pin<&mut Self>,
149            cx: &mut Context<'_>,
150        ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
151            let mut this = self.project();
152
153            if *this.frames_remaining == 0 {
154                return Poll::Ready(None);
155            }
156
157            // Start the sleep if not active.
158            let sleep_pinned = if let Some(s) = this.sleep.as_mut().as_pin_mut() {
159                s
160            } else {
161                this.sleep.set(Some(sleep(*this.frame_interval)));
162                this.sleep
163                    .as_mut()
164                    .as_pin_mut()
165                    .expect("Some value to be set in previous statement")
166            };
167
168            ready!(sleep_pinned.poll(cx));
169            this.sleep.set(None);
170            *this.frames_remaining -= 1;
171
172            Poll::Ready(Some(Ok(Frame::data(Bytes::from("chunk")))))
173        }
174    }
175
176    #[tokio::test(start_paused = true)]
177    async fn body_completes_within_timeout() {
178        let mock_body = MockBody {
179            sleep: sleep(Duration::from_millis(50)),
180        };
181        let timeout_body = DeadlineBody::new(Duration::from_millis(200), mock_body);
182
183        timeout_body
184            .boxed()
185            .frame()
186            .await
187            .expect("no frame")
188            .expect("frame should arrive before the deadline");
189    }
190
191    #[tokio::test(start_paused = true)]
192    async fn body_exceeds_timeout() {
193        let mock_body = MockBody {
194            sleep: sleep(Duration::from_millis(200)),
195        };
196        let timeout_body = DeadlineBody::new(Duration::from_millis(50), mock_body);
197
198        let result = timeout_body.boxed().frame().await.unwrap();
199        assert!(result.is_err());
200        assert!(result.unwrap_err().downcast_ref::<TimeoutError>().is_some());
201    }
202
203    #[tokio::test(start_paused = true)]
204    async fn deadline_fires_despite_steady_frames() {
205        // Each frame arrives every 30ms (well within an idle timeout of 100ms),
206        // but total transfer takes 5 * 30ms = 150ms, exceeding the 100ms deadline.
207        let body = MultiFrameBody {
208            frames_remaining: 5,
209            frame_interval: Duration::from_millis(30),
210            sleep: None,
211        };
212        let timeout_body = DeadlineBody::new(Duration::from_millis(100), body);
213
214        let mut boxed = timeout_body.boxed();
215        let mut got_error = false;
216
217        loop {
218            match boxed.frame().await {
219                Some(Ok(_)) => {}
220                Some(Err(_)) => {
221                    got_error = true;
222                    break;
223                }
224                None => break,
225            }
226        }
227
228        assert!(
229            got_error,
230            "expected timeout error before all frames arrived"
231        );
232    }
233
234    #[tokio::test(start_paused = true)]
235    async fn all_frames_arrive_within_deadline() {
236        // Each frame arrives every 20ms, total = 3 * 20ms = 60ms, within 200ms deadline.
237        let body = MultiFrameBody {
238            frames_remaining: 3,
239            frame_interval: Duration::from_millis(20),
240            sleep: None,
241        };
242        let timeout_body = DeadlineBody::new(Duration::from_millis(200), body);
243
244        let mut boxed = timeout_body.boxed();
245        let mut frame_count = 0;
246
247        loop {
248            match boxed.frame().await {
249                Some(Ok(_)) => frame_count += 1,
250                Some(Err(e)) => panic!("unexpected error: {e}"),
251                None => break,
252            }
253        }
254
255        assert_eq!(frame_count, 3);
256    }
257
258    /// A body that immediately yields a single inner error.
259    struct ErrBody;
260
261    impl StreamingBody for ErrBody {
262        type Data = Bytes;
263        type Error = MockError;
264
265        fn poll_frame(
266            self: Pin<&mut Self>,
267            _cx: &mut Context<'_>,
268        ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
269            Poll::Ready(Some(Err(MockError)))
270        }
271    }
272
273    #[tokio::test(start_paused = true)]
274    async fn inner_error_propagates_and_is_not_masked() {
275        // A genuine inner-body error before the deadline must surface as that error,
276        // not be swallowed or mistaken for a TimeoutError.
277        let body = DeadlineBody::new(Duration::from_millis(200), ErrBody);
278        let err = body.boxed().frame().await.unwrap().unwrap_err();
279        assert!(
280            err.downcast_ref::<MockError>().is_some(),
281            "inner error should be preserved"
282        );
283        assert!(
284            err.downcast_ref::<TimeoutError>().is_none(),
285            "inner error must not be reported as a deadline timeout"
286        );
287    }
288}