Skip to main content

rama_http/layer/timeout/
body.rs

1use crate::body::{Frame, StreamingBody};
2use pin_project_lite::pin_project;
3use rama_core::error::{BoxError, ErrorContext as _};
4use std::{
5    pin::Pin,
6    task::{Context, Poll, ready},
7    time::Duration,
8};
9use tokio::time::{Sleep, sleep};
10
11pin_project! {
12    /// Middleware that applies a timeout to request and response bodies.
13    ///
14    /// Wrapper around a [`Body`][`http_body::Body`] to time out if data is not ready within the specified duration.
15    /// The timeout is enforced between consecutive [`Frame`][`http_body::Frame`] polls, and it
16    /// resets after each poll.
17    /// The total time to produce a [`Body`][`http_body::Body`] could exceed the timeout duration without
18    /// timing out, as long as no single interval between polls exceeds the timeout.
19    ///
20    /// If the [`Body`][`http_body::Body`] does not produce a requested data frame within the timeout period, it will return a [`TimeoutError`].
21    ///
22    /// # Differences from [`Timeout`][crate::timeout::Timeout]
23    ///
24    /// [`Timeout`][crate::timeout::Timeout] applies a timeout to the request future, not body.
25    /// That timeout is not reset when bytes are handled, whether the request is active or not.
26    /// Bodies are handled asynchronously outside of the tower stack's future and thus needs an additional timeout.
27    ///
28    /// # Example
29    ///
30    /// ```
31    /// use rama_core::bytes::Bytes;
32    /// use std::time::Duration;
33    /// use rama_http::{Request, Response};
34    /// use rama_http::body::util::Full;
35    /// use rama_http::layer::timeout::RequestBodyTimeoutLayer;
36    /// use rama_core::{Layer, service::service_fn};
37    ///
38    /// async fn handle(_: Request<Full<Bytes>>) -> Result<Response<Full<Bytes>>, std::convert::Infallible> {
39    ///     // ...
40    ///     # todo!()
41    /// }
42    ///
43    /// # #[tokio::main]
44    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
45    /// let svc = RequestBodyTimeoutLayer::new(
46    ///     // Timeout bodies after 30 seconds of inactivity
47    ///     Duration::from_secs(30)
48    /// ).layer(service_fn(handle));
49    /// # Ok(())
50    /// # }
51    /// ```
52    pub struct TimeoutBody<B> {
53        timeout: Duration,
54        #[pin]
55        sleep: Option<Sleep>,
56        #[pin]
57        body: B,
58    }
59}
60
61impl<B> TimeoutBody<B> {
62    /// Creates a new [`TimeoutBody`].
63    pub fn new(timeout: Duration, body: B) -> Self {
64        Self {
65            timeout,
66            sleep: None,
67            body,
68        }
69    }
70}
71
72impl<B> StreamingBody for TimeoutBody<B>
73where
74    B: StreamingBody,
75    B::Error: Into<BoxError>,
76{
77    type Data = B::Data;
78    type Error = BoxError;
79
80    fn poll_frame(
81        self: Pin<&mut Self>,
82        cx: &mut Context<'_>,
83    ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
84        let mut this = self.project();
85
86        // Start the `Sleep` if not active.
87        let sleep_pinned = if let Some(some) = this.sleep.as_mut().as_pin_mut() {
88            some
89        } else {
90            this.sleep.set(Some(sleep(*this.timeout)));
91            #[expect(clippy::expect_used)]
92            this.sleep
93                .as_mut()
94                .as_pin_mut()
95                .expect("Some value to be set in previous statement (line)")
96        };
97
98        // Error if the timeout has expired.
99        if sleep_pinned.poll(cx) == Poll::Ready(()) {
100            return Poll::Ready(Some(Err(Box::new(TimeoutError(())))));
101        }
102
103        // Check for body data.
104        let frame = ready!(this.body.poll_frame(cx));
105        // A frame is ready. Reset the `Sleep`...
106        this.sleep.set(None);
107
108        Poll::Ready(frame.transpose().into_box_error().transpose())
109    }
110}
111
112/// Error for [`TimeoutBody`] and [`DeadlineBody`][super::DeadlineBody].
113#[derive(Debug)]
114pub struct TimeoutError(pub(super) ());
115
116impl std::error::Error for TimeoutError {}
117
118impl std::fmt::Display for TimeoutError {
119    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120        write!(f, "data was not received within the designated timeout")
121    }
122}
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    use crate::body::util::BodyExt;
128    use pin_project_lite::pin_project;
129    use rama_core::bytes::Bytes;
130    use std::{error::Error, fmt::Display};
131
132    #[derive(Debug)]
133    struct MockError;
134
135    impl Error for MockError {}
136
137    impl Display for MockError {
138        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139            write!(f, "mock error")
140        }
141    }
142
143    pin_project! {
144        struct MockBody {
145            #[pin]
146            sleep: Sleep
147        }
148    }
149
150    impl StreamingBody for MockBody {
151        type Data = Bytes;
152        type Error = MockError;
153
154        fn poll_frame(
155            self: Pin<&mut Self>,
156            cx: &mut Context<'_>,
157        ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
158            let this = self.project();
159            this.sleep
160                .poll(cx)
161                .map(|_| Some(Ok(Frame::data(vec![].into()))))
162        }
163    }
164
165    #[tokio::test]
166    async fn test_body_available_within_timeout() {
167        let mock_sleep = Duration::from_secs(1);
168        let timeout_sleep = Duration::from_secs(2);
169
170        let mock_body = MockBody {
171            sleep: sleep(mock_sleep),
172        };
173        let timeout_body = TimeoutBody::new(timeout_sleep, mock_body);
174
175        timeout_body
176            .boxed()
177            .frame()
178            .await
179            .expect("no frame")
180            .unwrap();
181    }
182
183    #[tokio::test]
184    async fn test_body_unavailable_within_timeout_error() {
185        let mock_sleep = Duration::from_secs(2);
186        let timeout_sleep = Duration::from_secs(1);
187
188        let mock_body = MockBody {
189            sleep: sleep(mock_sleep),
190        };
191        let timeout_body = TimeoutBody::new(timeout_sleep, mock_body);
192
193        timeout_body.boxed().frame().await.unwrap().unwrap_err();
194    }
195}