rama_http/layer/timeout/
deadline_body.rs1use 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 pub struct DeadlineBody<B> {
36 #[pin]
37 sleep: Sleep,
38 #[pin]
39 body: B,
40 }
41}
42
43impl<B> DeadlineBody<B> {
44 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 if this.sleep.poll(cx) == Poll::Ready(()) {
72 return Poll::Ready(Some(Err(Box::new(TimeoutError(())))));
73 }
74
75 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 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 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 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 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 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 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 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}