nautilus_network/http/
stream.rs1use bytes::Bytes;
19use http::StatusCode;
20use http_body_util::BodyExt;
21use hyper::body::Incoming;
22
23use super::{HttpClientError, client::REQUEST_TIMEOUT_MESSAGE, error::transport_error};
24use crate::dst::time::Instant;
25
26#[derive(Debug)]
32pub struct HttpResponseStream {
33 pub(super) response: http::Response<Incoming>,
34 pub(super) deadline: Option<Instant>,
35 pub(super) origin: Option<String>,
36 #[cfg(all(feature = "simulation", madsim))]
37 pub(super) _connection: super::simulation::Connection,
38}
39
40impl HttpResponseStream {
41 #[must_use]
43 pub fn status(&self) -> StatusCode {
44 self.response.status()
45 }
46
47 pub async fn chunk(&mut self) -> Result<Option<Bytes>, HttpClientError> {
53 read_chunk(self.response.body_mut(), self.deadline)
54 .await
55 .map_err(|e| response_error(e, self.origin.as_deref()))
56 }
57}
58
59pub(super) async fn read_chunk<B>(
60 body: &mut B,
61 deadline: Option<Instant>,
62) -> Result<Option<Bytes>, HttpClientError>
63where
64 B: http_body::Body<Data = Bytes> + Unpin,
65 B::Error: std::error::Error + 'static,
66{
67 loop {
68 check_deadline(deadline)?;
69
70 let frame = match deadline {
71 Some(deadline) => {
72 tokio::select! {
73 biased;
74 () = crate::dst::time::sleep_until(deadline) => return Err(HttpClientError::TimeoutError(REQUEST_TIMEOUT_MESSAGE.into())),
75 frame = body.frame() => frame,
76 }
77 }
78 None => body.frame().await,
79 };
80
81 let Some(frame) = frame else {
82 return Ok(None);
83 };
84
85 if let Ok(chunk) = frame.map_err(|e| transport_error(&e))?.into_data() {
86 return Ok(Some(chunk));
87 }
88 }
89}
90
91pub(super) fn check_deadline(deadline: Option<Instant>) -> Result<(), HttpClientError> {
92 if deadline.is_some_and(|deadline| Instant::now() >= deadline) {
93 return Err(HttpClientError::TimeoutError(
94 REQUEST_TIMEOUT_MESSAGE.into(),
95 ));
96 }
97
98 Ok(())
99}
100
101pub(super) fn response_error(error: HttpClientError, origin: Option<&str>) -> HttpClientError {
102 match (error, origin) {
103 (HttpClientError::TransportError(message), Some(origin)) => {
104 HttpClientError::TransportError(format!("{message} for origin ({origin})"))
105 }
106 (error, _) => error,
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use std::io;
113
114 use http::{HeaderMap, HeaderValue, header::HeaderName};
115 use http_body::Frame;
116 use http_body_util::StreamBody;
117 use rstest::rstest;
118
119 use super::*;
120
121 #[tokio::test]
122 async fn read_chunk_skips_trailers_and_preserves_data() {
123 let trailers = HeaderMap::from_iter([(
124 HeaderName::from_static("x-checksum"),
125 HeaderValue::from_static("receipt-83"),
126 )]);
127 let frames: Vec<Result<_, io::Error>> = vec![
128 Ok(Frame::data(Bytes::from_static(b"first"))),
129 Ok(Frame::data(Bytes::from_static(b"second"))),
130 Ok(Frame::trailers(trailers)),
131 ];
132 let mut body = StreamBody::new(futures_util::stream::iter(frames));
133
134 let first = read_chunk(&mut body, None).await.unwrap();
135 let second = read_chunk(&mut body, None).await.unwrap();
136 let end = read_chunk(&mut body, None).await.unwrap();
137
138 assert_eq!(first, Some(Bytes::from_static(b"first")));
139 assert_eq!(second, Some(Bytes::from_static(b"second")));
140 assert_eq!(end, None);
141 }
142
143 #[rstest]
144 #[case::transport(io::ErrorKind::UnexpectedEof, false)]
145 #[case::timeout(io::ErrorKind::TimedOut, true)]
146 #[tokio::test]
147 async fn read_chunk_propagates_body_error(#[case] kind: io::ErrorKind, #[case] timeout: bool) {
148 let frames = vec![Err::<Frame<Bytes>, _>(io::Error::new(kind, "body failure"))];
149 let mut body = StreamBody::new(futures_util::stream::iter(frames));
150
151 let error = read_chunk(&mut body, None).await.unwrap_err();
152
153 match (error, timeout) {
154 (HttpClientError::TimeoutError(message), true)
155 | (HttpClientError::TransportError(message), false) => {
156 assert_eq!(message, "body failure");
157 }
158 (error, _) => panic!("unexpected classification: {error:?}"),
159 }
160 }
161}