use std::{
future::Future,
pin::Pin,
task::{Context, Poll, ready},
time::Duration,
};
use http_body::Body;
use pin_project_lite::pin_project;
use tokio::time::{Sleep, sleep};
use crate::{
Error,
error::{BoxError, TimedOut},
};
pin_project! {
#[project = TimeoutBodyProj]
pub enum TimeoutBody<B> {
Plain {
#[pin]
body: B,
},
TotalTimeout {
#[pin]
body: TotalTimeoutBody<B>,
},
ReadTimeout {
#[pin]
body: ReadTimeoutBody<B>
},
CombinedTimeout {
#[pin]
body: TotalTimeoutBody<ReadTimeoutBody<B>>,
}
}
}
pin_project! {
pub struct TotalTimeoutBody<B> {
#[pin]
body: B,
timeout: Pin<Box<Sleep>>,
}
}
pin_project! {
pub struct ReadTimeoutBody<B> {
timeout: Duration,
sleep: Pin<Box<Sleep>>,
#[pin]
body: B,
}
}
impl<B> TimeoutBody<B> {
pub(crate) fn new(deadline: Option<Duration>, read_timeout: Option<Duration>, body: B) -> Self {
let deadline = deadline.map(sleep).map(Box::pin);
match (deadline, read_timeout) {
(Some(total_timeout), Some(read_timeout)) => Self::CombinedTimeout {
body: TotalTimeoutBody {
timeout: total_timeout,
body: ReadTimeoutBody {
timeout: read_timeout,
sleep: Box::pin(sleep(read_timeout)),
body,
},
},
},
(Some(timeout), None) => Self::TotalTimeout {
body: TotalTimeoutBody { body, timeout },
},
(None, Some(timeout)) => Self::ReadTimeout {
body: ReadTimeoutBody {
timeout,
sleep: Box::pin(sleep(timeout)),
body,
},
},
(None, None) => Self::Plain { body },
}
}
}
impl<B> Body for TimeoutBody<B>
where
B: Body,
B::Error: Into<BoxError>,
{
type Data = B::Data;
type Error = BoxError;
#[inline]
fn poll_frame(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<http_body::Frame<Self::Data>, Self::Error>>> {
match self.project() {
TimeoutBodyProj::TotalTimeout { body } => body.poll_frame(cx),
TimeoutBodyProj::ReadTimeout { body } => body.poll_frame(cx),
TimeoutBodyProj::CombinedTimeout { body } => body.poll_frame(cx),
TimeoutBodyProj::Plain { body } => poll_and_map_body(body, cx),
}
}
#[inline]
fn size_hint(&self) -> http_body::SizeHint {
match self {
Self::TotalTimeout { body } => body.size_hint(),
Self::ReadTimeout { body } => body.size_hint(),
Self::CombinedTimeout { body } => body.size_hint(),
Self::Plain { body } => body.size_hint(),
}
}
#[inline]
fn is_end_stream(&self) -> bool {
match self {
Self::TotalTimeout { body } => body.is_end_stream(),
Self::ReadTimeout { body } => body.is_end_stream(),
Self::CombinedTimeout { body } => body.is_end_stream(),
Self::Plain { body } => body.is_end_stream(),
}
}
}
#[inline]
fn poll_and_map_body<B>(
body: Pin<&mut B>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<http_body::Frame<B::Data>, BoxError>>>
where
B: Body,
B::Error: Into<BoxError>,
{
Poll::Ready(ready!(body.poll_frame(cx)).map(|opt| opt.map_err(Error::body).map_err(Into::into)))
}
impl<B> Body for TotalTimeoutBody<B>
where
B: Body,
B::Error: Into<BoxError>,
{
type Data = B::Data;
type Error = BoxError;
fn poll_frame(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<http_body::Frame<Self::Data>, Self::Error>>> {
let this = self.project();
if this.timeout.as_mut().poll(cx) == Poll::Ready(()) {
return Poll::Ready(Some(Err(Error::body(TimedOut).into())));
}
poll_and_map_body(this.body, cx)
}
#[inline]
fn size_hint(&self) -> http_body::SizeHint {
self.body.size_hint()
}
#[inline]
fn is_end_stream(&self) -> bool {
self.body.is_end_stream()
}
}
impl<B> Body for ReadTimeoutBody<B>
where
B: Body,
B::Error: Into<BoxError>,
{
type Data = B::Data;
type Error = BoxError;
fn poll_frame(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<http_body::Frame<Self::Data>, Self::Error>>> {
let this = self.project();
if this.sleep.as_mut().poll(cx).is_ready() {
return Poll::Ready(Some(Err(Box::new(TimedOut))));
}
match ready!(this.body.poll_frame(cx)) {
Some(Ok(frame)) => {
let deadline = tokio::time::Instant::now() + *this.timeout;
this.sleep.as_mut().reset(deadline);
Poll::Ready(Some(Ok(frame)))
}
Some(Err(err)) => Poll::Ready(Some(Err(err.into()))),
None => Poll::Ready(None),
}
}
#[inline]
fn size_hint(&self) -> http_body::SizeHint {
self.body.size_hint()
}
#[inline]
fn is_end_stream(&self) -> bool {
self.body.is_end_stream()
}
}
#[cfg(test)]
mod tests {
use std::{
pin::Pin,
task::{Context, Poll},
time::Duration,
};
use bytes::Bytes;
use http_body::{Body, Frame};
use tokio::time::sleep;
use super::{ReadTimeoutBody, TimeoutBody};
use crate::error::TimedOut;
struct ImmediateBody {
remaining: usize,
}
impl Body for ImmediateBody {
type Data = Bytes;
type Error = std::convert::Infallible;
fn poll_frame(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
if self.remaining == 0 {
Poll::Ready(None)
} else {
self.remaining -= 1;
Poll::Ready(Some(Ok(Frame::data(Bytes::from_static(b"chunk")))))
}
}
}
struct StalledBody;
impl Body for StalledBody {
type Data = Bytes;
type Error = std::convert::Infallible;
fn poll_frame(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
Poll::Pending
}
}
#[tokio::test]
async fn read_timeout_body_delivers_fast_stream_without_false_trigger() {
let inner = ImmediateBody { remaining: 2 };
let mut body = Box::pin(ReadTimeoutBody {
timeout: Duration::from_millis(100),
sleep: Box::pin(sleep(Duration::from_millis(100))),
body: inner,
});
for _ in 0..2 {
let frame = futures_util::future::poll_fn(|cx| body.as_mut().poll_frame(cx)).await;
assert!(frame.is_some());
assert!(frame.unwrap().is_ok());
}
let end = futures_util::future::poll_fn(|cx| body.as_mut().poll_frame(cx)).await;
assert!(end.is_none());
}
#[tokio::test]
async fn read_timeout_body_still_triggers_on_stalled_stream() {
let mut body = Box::pin(ReadTimeoutBody {
timeout: Duration::from_millis(10),
sleep: Box::pin(sleep(Duration::from_millis(10))),
body: StalledBody,
});
let frame = tokio::time::timeout(
Duration::from_secs(5),
futures_util::future::poll_fn(|cx| body.as_mut().poll_frame(cx)),
)
.await
.expect("stalled read must resolve via read timeout");
let err = frame.expect("timeout returns an error frame").unwrap_err();
assert!(
err.downcast_ref::<TimedOut>().is_some(),
"expected TimedOut, got {err:?}"
);
}
#[tokio::test]
async fn read_timeout_body_resets_deadline_between_chunks() {
let inner = ImmediateBody { remaining: 3 };
let mut body = Box::pin(TimeoutBody::new(
None,
Some(Duration::from_millis(50)),
inner,
));
for _ in 0..3 {
let frame = futures_util::future::poll_fn(|cx| body.as_mut().poll_frame(cx)).await;
assert!(frame.is_some());
assert!(frame.unwrap().is_ok());
}
let end = futures_util::future::poll_fn(|cx| body.as_mut().poll_frame(cx)).await;
assert!(end.is_none());
}
}