1use std::future::Future;
2use std::pin::Pin;
3use std::task::{Context, Poll};
4use std::time::Duration;
5
6use bytes::Bytes;
7use futures_core::Stream;
8use futures_timer::Delay;
9use futures_util::future::{Either, select};
10
11use crate::{Body, HttpRequest, HttpResponse, HttpTransport, TransportError, TransportErrorKind};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59#[non_exhaustive]
60pub struct StreamTimeouts {
61 pub setup: Option<Duration>,
63 pub idle: Option<Duration>,
66}
67
68impl Default for StreamTimeouts {
69 fn default() -> Self {
72 Self {
73 setup: Some(Self::DEFAULT_SETUP),
74 idle: Some(Self::DEFAULT_IDLE),
75 }
76 }
77}
78
79impl StreamTimeouts {
80 pub const DEFAULT_SETUP: Duration = Duration::from_secs(120);
85
86 pub const DEFAULT_IDLE: Duration = Duration::from_secs(300);
91
92 #[must_use]
95 pub const fn unbounded() -> Self {
96 Self {
97 setup: None,
98 idle: None,
99 }
100 }
101
102 #[must_use]
104 pub const fn setup(mut self, timeout: Option<Duration>) -> Self {
105 self.setup = timeout;
106 self
107 }
108
109 #[must_use]
111 pub const fn idle(mut self, timeout: Option<Duration>) -> Self {
112 self.idle = timeout;
113 self
114 }
115
116 pub async fn send<T>(
127 self,
128 transport: &T,
129 request: HttpRequest,
130 ) -> Result<HttpResponse, TransportError>
131 where
132 T: HttpTransport + ?Sized,
133 {
134 let sent = transport.send(request);
135 let response = match self.setup {
136 Some(limit) => match select(sent, Delay::new(limit)).await {
137 Either::Left((result, _)) => result?,
138 Either::Right(((), _)) => return Err(expired("stream setup", limit)),
139 },
140 None => sent.await?,
141 };
142 let Some(idle) = self.idle else {
143 return Ok(response);
144 };
145 let status = response.status;
146 let headers = response.headers.clone();
147 let body = IdleTimeoutBody::new(response.into_body(), idle);
148 Ok(HttpResponse::new(status, headers, Box::pin(body)))
149 }
150}
151
152struct IdleTimeoutBody {
160 inner: Option<Body>,
161 idle: Duration,
162 timer: Delay,
163 armed: bool,
166}
167
168impl IdleTimeoutBody {
169 fn new(inner: Body, idle: Duration) -> Self {
170 Self {
171 inner: Some(inner),
172 idle,
173 timer: Delay::new(idle),
174 armed: false,
177 }
178 }
179}
180
181impl Stream for IdleTimeoutBody {
182 type Item = Result<Bytes, TransportError>;
183
184 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
185 let this = &mut *self;
186 let Some(inner) = this.inner.as_mut() else {
187 return Poll::Ready(None);
188 };
189 match inner.as_mut().poll_next(cx) {
190 Poll::Ready(Some(item)) => {
191 this.armed = false;
192 Poll::Ready(Some(item))
193 }
194 Poll::Ready(None) => {
195 this.inner = None;
196 Poll::Ready(None)
197 }
198 Poll::Pending => {
199 if !this.armed {
200 this.timer.reset(this.idle);
201 this.armed = true;
202 }
203 if Pin::new(&mut this.timer).poll(cx).is_pending() {
204 return Poll::Pending;
205 }
206 this.inner = None;
207 Poll::Ready(Some(Err(expired(
208 "wait for the next body chunk",
209 this.idle,
210 ))))
211 }
212 }
213 }
214}
215
216fn expired(what: &str, limit: Duration) -> TransportError {
219 TransportError::new(
220 TransportErrorKind::Timeout,
221 format!("{what} exceeded {limit:?}"),
222 )
223 .authored()
224}
225
226#[cfg(all(test, not(target_arch = "wasm32")))]
227mod tests {
228 use futures_util::StreamExt;
229
230 use super::*;
231 use crate::{Headers, ReplayResponse, ReplayTransport, SendFuture};
232
233 const SHORT: Duration = Duration::from_millis(20);
234
235 struct Slow {
238 headers_after: Duration,
239 gap: Option<Duration>,
240 }
241
242 impl HttpTransport for Slow {
243 fn send(&self, _request: HttpRequest) -> SendFuture<'_> {
244 let gap = self.gap;
245 let headers_after = self.headers_after;
246 Box::pin(async move {
247 Delay::new(headers_after).await;
248 let body: Body = match gap {
249 Some(gap) => Box::pin(futures_util::stream::iter(["a", "b", "c"]).then(
250 move |chunk| async move {
251 Delay::new(gap).await;
252 Ok(Bytes::from_static(chunk.as_bytes()))
253 },
254 )),
255 None => Box::pin(futures_util::stream::pending()),
256 };
257 Ok(HttpResponse::new(200, Headers::new(), body))
258 })
259 }
260 }
261
262 fn request() -> HttpRequest {
263 HttpRequest::post("https://x", "{}")
264 }
265
266 #[tokio::test]
267 async fn slow_headers_fail_the_setup_bound() {
268 let transport = Slow {
269 headers_after: Duration::from_secs(5),
270 gap: Some(Duration::ZERO),
271 };
272 let err = StreamTimeouts::unbounded()
273 .setup(Some(SHORT))
274 .send(&transport, request())
275 .await
276 .unwrap_err();
277 assert_eq!(err.kind(), TransportErrorKind::Timeout);
278 let err = err.into_error();
279 assert!(matches!(err, allama::Error::Timeout { .. }));
280 assert_eq!(err.message(), "stream setup exceeded 20ms");
281 assert_eq!(
283 err.as_log().to_string(),
284 r#"kind=timeout message="stream setup exceeded 20ms""#
285 );
286 }
287
288 #[tokio::test]
289 async fn a_stalled_body_fails_the_idle_bound_then_ends() {
290 let transport = Slow {
291 headers_after: Duration::ZERO,
292 gap: None,
293 };
294 let response = StreamTimeouts::default()
295 .idle(Some(SHORT))
296 .send(&transport, request())
297 .await
298 .unwrap();
299 assert_eq!(response.status, 200);
300 let mut body = response.into_body();
301 let err = body.next().await.unwrap().unwrap_err();
302 assert_eq!(err.kind(), TransportErrorKind::Timeout);
303 assert_eq!(
304 err.to_string(),
305 "request timed out: wait for the next body chunk exceeded 20ms"
306 );
307 assert!(body.next().await.is_none());
308 }
309
310 #[tokio::test]
311 async fn steady_chunks_reset_the_idle_bound() {
312 let transport = Slow {
313 headers_after: Duration::ZERO,
314 gap: Some(Duration::from_millis(30)),
315 };
316 let response = StreamTimeouts::unbounded()
318 .idle(Some(Duration::from_millis(60)))
319 .send(&transport, request())
320 .await
321 .unwrap();
322 assert_eq!(response.text().await.unwrap(), "abc");
323 }
324
325 #[tokio::test]
329 async fn slow_consumers_do_not_trip_the_idle_bound() {
330 let transport = Slow {
331 headers_after: Duration::ZERO,
332 gap: Some(SHORT),
333 };
334 let response = StreamTimeouts::unbounded()
335 .idle(Some(Duration::from_millis(100)))
336 .send(&transport, request())
337 .await
338 .unwrap();
339 let mut body = response.into_body();
340 let mut chunks = Vec::new();
341 while let Some(chunk) = body.next().await {
342 chunks.push(chunk.unwrap());
343 Delay::new(Duration::from_millis(150)).await;
344 }
345 assert_eq!(chunks.concat(), b"abc");
346 }
347
348 #[tokio::test]
351 async fn a_late_first_poll_does_not_trip_the_idle_bound() {
352 let transport = Slow {
353 headers_after: Duration::ZERO,
354 gap: Some(Duration::from_millis(10)),
355 };
356 let response = StreamTimeouts::unbounded()
357 .idle(Some(Duration::from_millis(50)))
358 .send(&transport, request())
359 .await
360 .unwrap();
361 Delay::new(Duration::from_millis(100)).await;
364 assert_eq!(response.text().await.unwrap(), "abc");
365 }
366
367 #[tokio::test]
370 async fn a_stalled_first_chunk_fails_the_idle_bound() {
371 let body: Body = Box::pin(futures_util::stream::pending());
372 let mut body = IdleTimeoutBody::new(body, Duration::from_millis(50));
373 Delay::new(Duration::from_millis(100)).await;
374 let started = std::time::Instant::now();
375 let err = body.next().await.unwrap().unwrap_err();
376 assert_eq!(err.kind(), TransportErrorKind::Timeout);
377 assert!(
378 started.elapsed() >= Duration::from_millis(50),
379 "{:?}",
380 started.elapsed()
381 );
382 assert!(body.next().await.is_none());
383 }
384
385 #[tokio::test]
388 async fn a_body_stalled_after_a_chunk_fails_the_idle_bound() {
389 let body: Body = Box::pin(
390 futures_util::stream::iter([Ok(Bytes::from_static(b"a"))])
391 .chain(futures_util::stream::pending()),
392 );
393 let mut body = IdleTimeoutBody::new(body, SHORT);
394 assert_eq!(body.next().await.unwrap().unwrap(), "a");
395 Delay::new(SHORT * 3).await;
396 let started = std::time::Instant::now();
397 let err = body.next().await.unwrap().unwrap_err();
398 assert_eq!(err.kind(), TransportErrorKind::Timeout);
399 assert!(started.elapsed() >= SHORT, "{:?}", started.elapsed());
400 assert!(body.next().await.is_none());
401 }
402
403 #[tokio::test]
404 async fn unbounded_passes_responses_and_errors_through() {
405 let transport = ReplayTransport::new([ReplayResponse::new(201, "ok")]);
406 let response = StreamTimeouts::unbounded()
407 .send(&transport, request())
408 .await
409 .unwrap();
410 assert_eq!(response.status, 201);
411 assert_eq!(response.text().await.unwrap(), "ok");
412
413 let err = StreamTimeouts::default()
415 .send(&transport, request())
416 .await
417 .unwrap_err();
418 assert_eq!(err.kind(), TransportErrorKind::Connect);
419 }
420}
421
422#[cfg(all(test, target_arch = "wasm32"))]
425mod wasm_tests {
426 use futures_util::StreamExt;
427 use wasm_bindgen_test::wasm_bindgen_test;
428
429 use super::*;
430 use crate::{Headers, SendFuture};
431
432 struct Stalled;
433
434 impl HttpTransport for Stalled {
435 fn send(&self, _request: HttpRequest) -> SendFuture<'_> {
436 Box::pin(async {
437 Ok(HttpResponse::new(
438 200,
439 Headers::new(),
440 Box::pin(futures_util::stream::pending()),
441 ))
442 })
443 }
444 }
445
446 #[wasm_bindgen_test]
447 async fn a_stalled_body_fails_the_idle_bound() {
448 let response = StreamTimeouts::default()
449 .idle(Some(Duration::from_millis(20)))
450 .send(&Stalled, HttpRequest::get("https://x"))
451 .await
452 .unwrap();
453 let err = response.into_body().next().await.unwrap().unwrap_err();
454 assert_eq!(err.kind(), TransportErrorKind::Timeout);
455 }
456}