use bytes::Bytes;
use http::StatusCode;
use http_body_util::BodyExt;
use hyper::body::Incoming;
use url::Url;
use super::{HttpClientError, client::REQUEST_TIMEOUT_MESSAGE, error::transport_error};
use crate::dst::time::Instant;
#[derive(Debug)]
pub struct HttpResponseStream {
pub(super) response: http::Response<Incoming>,
pub(super) deadline: Option<Instant>,
pub(super) url: Option<Url>,
#[cfg(all(feature = "simulation", madsim))]
pub(super) _connection: super::simulation::Connection,
}
impl HttpResponseStream {
#[must_use]
pub fn status(&self) -> StatusCode {
self.response.status()
}
pub async fn chunk(&mut self) -> Result<Option<Bytes>, HttpClientError> {
read_chunk(self.response.body_mut(), self.deadline)
.await
.map_err(|e| response_error(e, self.url.as_ref()))
}
}
pub(super) async fn read_chunk<B>(
body: &mut B,
deadline: Option<Instant>,
) -> Result<Option<Bytes>, HttpClientError>
where
B: http_body::Body<Data = Bytes> + Unpin,
B::Error: std::error::Error + 'static,
{
loop {
let frame = match deadline {
Some(deadline) => {
if Instant::now() >= deadline {
return Err(HttpClientError::TimeoutError(
REQUEST_TIMEOUT_MESSAGE.into(),
));
}
tokio::select! {
biased;
() = crate::dst::time::sleep_until(deadline) => return Err(HttpClientError::TimeoutError(REQUEST_TIMEOUT_MESSAGE.into())),
frame = body.frame() => frame,
}
}
None => body.frame().await,
};
let Some(frame) = frame else {
return Ok(None);
};
if let Ok(chunk) = frame.map_err(|e| transport_error(&e))?.into_data() {
return Ok(Some(chunk));
}
}
}
pub(super) fn response_error(error: HttpClientError, url: Option<&Url>) -> HttpClientError {
match (error, url) {
(HttpClientError::TransportError(message), Some(url)) => {
HttpClientError::TransportError(format!("{message} for url ({url})"))
}
(error, _) => error,
}
}
#[cfg(test)]
mod tests {
use std::io;
use http::{HeaderMap, HeaderValue, header::HeaderName};
use http_body::Frame;
use http_body_util::StreamBody;
use rstest::rstest;
use super::*;
#[tokio::test]
async fn read_chunk_skips_trailers_and_preserves_data() {
let trailers = HeaderMap::from_iter([(
HeaderName::from_static("x-checksum"),
HeaderValue::from_static("receipt-83"),
)]);
let frames: Vec<Result<_, io::Error>> = vec![
Ok(Frame::data(Bytes::from_static(b"first"))),
Ok(Frame::data(Bytes::from_static(b"second"))),
Ok(Frame::trailers(trailers)),
];
let mut body = StreamBody::new(futures_util::stream::iter(frames));
let first = read_chunk(&mut body, None).await.unwrap();
let second = read_chunk(&mut body, None).await.unwrap();
let end = read_chunk(&mut body, None).await.unwrap();
assert_eq!(first, Some(Bytes::from_static(b"first")));
assert_eq!(second, Some(Bytes::from_static(b"second")));
assert_eq!(end, None);
}
#[rstest]
#[case::transport(io::ErrorKind::UnexpectedEof, false)]
#[case::timeout(io::ErrorKind::TimedOut, true)]
#[tokio::test]
async fn read_chunk_propagates_body_error(#[case] kind: io::ErrorKind, #[case] timeout: bool) {
let frames = vec![Err::<Frame<Bytes>, _>(io::Error::new(kind, "body failure"))];
let mut body = StreamBody::new(futures_util::stream::iter(frames));
let error = read_chunk(&mut body, None).await.unwrap_err();
match (error, timeout) {
(HttpClientError::TimeoutError(message), true)
| (HttpClientError::TransportError(message), false) => {
assert_eq!(message, "body failure");
}
(error, _) => panic!("unexpected classification: {error:?}"),
}
}
}