mkit_server/http_objects/
body.rs1use core::pin::Pin;
4use core::task::{Context, Poll};
5
6use bytes::Bytes;
7use futures_core::Stream;
8
9use crate::store::StoreError;
10use crate::{BoxStream, Code, ServerError};
11
12pub enum HttpBody {
15 Empty,
17 Bytes(Bytes),
19 Stream {
21 len: u64,
23 stream: BoxStream<'static, Result<Bytes, ServerError>>,
25 },
26}
27
28impl core::fmt::Debug for HttpBody {
29 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
30 match self {
31 Self::Empty => f.write_str("Empty"),
32 Self::Bytes(b) => f.debug_tuple("Bytes").field(&b.len()).finish(),
33 Self::Stream { len, .. } => f.debug_struct("Stream").field("len", len).finish(),
34 }
35 }
36}
37
38#[cfg(not(target_arch = "wasm32"))]
42pub type EndHook = Box<dyn FnOnce(u64, Result<(), &ServerError>) + Send>;
43#[cfg(target_arch = "wasm32")]
45pub type EndHook = Box<dyn FnOnce(u64, Result<(), &ServerError>)>;
46
47fn overrun() -> ServerError {
48 ServerError::unavailable("object body length changed")
49}
50
51struct Exact {
53 inner: BoxStream<'static, Result<Bytes, ServerError>>,
54 remaining: u64,
55 sent: u64,
56 hook: Option<EndHook>,
57 done: bool,
58}
59
60impl Exact {
61 fn end(&mut self, result: Result<(), &ServerError>) {
62 self.done = true;
63 if let Some(hook) = self.hook.take() {
64 hook(self.sent, result);
65 }
66 }
67
68 fn fail(&mut self, error: ServerError) -> Poll<Option<Result<Bytes, ServerError>>> {
69 self.end(Err(&error));
70 Poll::Ready(Some(Err(error)))
71 }
72}
73
74impl Stream for Exact {
75 type Item = Result<Bytes, ServerError>;
76
77 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
78 if self.done {
79 return Poll::Ready(None);
80 }
81 match self.inner.as_mut().poll_next(cx) {
82 Poll::Pending => Poll::Pending,
83 Poll::Ready(Some(Ok(piece))) => {
84 let n = piece.len() as u64;
85 if n > self.remaining {
86 return self.fail(overrun());
87 }
88 self.remaining -= n;
89 self.sent += n;
90 Poll::Ready(Some(Ok(piece)))
91 }
92 Poll::Ready(Some(Err(error))) => self.fail(error),
93 Poll::Ready(None) if self.remaining != 0 => self.fail(overrun()),
94 Poll::Ready(None) => {
95 self.end(Ok(()));
96 Poll::Ready(None)
97 }
98 }
99 }
100}
101
102impl Drop for Exact {
103 fn drop(&mut self) {
104 if !self.done {
105 self.end(Err(&ServerError::new(Code::Canceled, "request canceled")));
106 }
107 }
108}
109
110fn wrap(
111 stream: BoxStream<'static, Result<Bytes, ServerError>>,
112 len: u64,
113 hook: Option<EndHook>,
114) -> HttpBody {
115 HttpBody::Stream {
116 len,
117 stream: Box::pin(Exact {
118 inner: stream,
119 remaining: len,
120 sent: 0,
121 hook,
122 done: false,
123 }),
124 }
125}
126
127#[must_use]
131pub fn exact(
132 stream: BoxStream<'static, Result<Bytes, StoreError>>,
133 len: u64,
134 hook: Option<EndHook>,
135) -> HttpBody {
136 let stream = futures::StreamExt::map(stream, |piece| {
137 piece.map_err(|error| {
138 tracing::warn!(detail = %error, "object body read failed");
139 ServerError::unavailable("object storage request failed")
140 })
141 });
142 wrap(Box::pin(stream), len, hook)
143}
144
145#[must_use]
148pub fn with_hook(body: HttpBody, hook: Option<EndHook>) -> HttpBody {
149 let Some(hook) = hook else {
150 return body;
151 };
152 match body {
153 HttpBody::Empty => {
154 hook(0, Ok(()));
155 HttpBody::Empty
156 }
157 HttpBody::Bytes(bytes) => {
158 let len = bytes.len() as u64;
159 wrap(
160 Box::pin(futures::stream::once(core::future::ready(Ok(bytes)))),
161 len,
162 Some(hook),
163 )
164 }
165 HttpBody::Stream { len, stream } => wrap(stream, len, Some(hook)),
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 #![allow(clippy::unwrap_used)]
172
173 use std::sync::{Arc, Mutex};
174
175 use futures::StreamExt as _;
176 use futures_executor::block_on;
177
178 use super::*;
179
180 type Ended = Arc<Mutex<Vec<(u64, Option<Code>)>>>;
181
182 fn hook(ended: &Ended) -> EndHook {
183 let ended = ended.clone();
184 Box::new(move |sent, result| {
185 ended
186 .lock()
187 .unwrap()
188 .push((sent, result.err().map(ServerError::code)));
189 })
190 }
191
192 fn source(
193 pieces: Vec<Result<&'static [u8], StoreError>>,
194 ) -> BoxStream<'static, Result<Bytes, StoreError>> {
195 Box::pin(futures::stream::iter(
196 pieces
197 .into_iter()
198 .map(|piece| piece.map(Bytes::from_static))
199 .collect::<Vec<_>>(),
200 ))
201 }
202
203 fn collect(body: HttpBody) -> Vec<Result<usize, Code>> {
204 let HttpBody::Stream { stream, .. } = body else {
205 panic!("expected a stream");
206 };
207 block_on(
208 stream
209 .map(|p| p.map(|b| b.len()).map_err(|e| e.code()))
210 .collect(),
211 )
212 }
213
214 #[test]
215 fn an_exact_body_reports_its_end_once() {
216 let ended = Ended::default();
217 let body = exact(source(vec![Ok(b"abc"), Ok(b"de")]), 5, Some(hook(&ended)));
218 assert_eq!(collect(body), vec![Ok(3), Ok(2)]);
219 assert_eq!(*ended.lock().unwrap(), [(5, None)]);
220 }
221
222 #[test]
223 fn a_wrong_length_or_a_backend_failure_fails_the_body_and_the_hook() {
224 for (pieces, len, sent, want) in [
225 (vec![Ok(&b"abcd"[..])], 3, 0, vec![Err(Code::Unavailable)]),
226 (
227 vec![Ok(&b"ab"[..])],
228 3,
229 2,
230 vec![Ok(2), Err(Code::Unavailable)],
231 ),
232 (
233 vec![
234 Ok(&b"ab"[..]),
235 Err(StoreError::unavailable("SECRET backend detail")),
236 ],
237 4,
238 2,
239 vec![Ok(2), Err(Code::Unavailable)],
240 ),
241 ] {
242 let ended = Ended::default();
243 let got = collect(exact(source(pieces), len, Some(hook(&ended))));
244 assert_eq!(got, want);
245 assert_eq!(*ended.lock().unwrap(), [(sent, Some(Code::Unavailable))]);
246 }
247 }
248
249 #[test]
250 fn a_dropped_body_reports_canceled_with_what_was_sent() {
251 let ended = Ended::default();
252 let HttpBody::Stream { mut stream, .. } =
253 exact(source(vec![Ok(b"abc"), Ok(b"def")]), 6, Some(hook(&ended)))
254 else {
255 panic!();
256 };
257 assert_eq!(block_on(stream.next()).unwrap().unwrap().len(), 3);
258 drop(stream);
259 assert_eq!(*ended.lock().unwrap(), [(3, Some(Code::Canceled))]);
260 }
261
262 #[test]
263 fn a_backend_error_is_redacted_to_a_fixed_message() {
264 let body = exact(
265 source(vec![Err(StoreError::unavailable("SECRET backend detail"))]),
266 1,
267 None,
268 );
269 let HttpBody::Stream { mut stream, .. } = body else {
270 panic!();
271 };
272 let error = block_on(stream.next()).unwrap().unwrap_err();
273 assert!(!format!("{error:?} {error}").contains("SECRET"));
274 assert_eq!(error.public_message(), "object storage request failed");
275 }
276
277 #[test]
278 fn a_hook_also_covers_in_memory_and_empty_bodies() {
279 let ended = Ended::default();
280 let body = with_hook(
281 HttpBody::Bytes(Bytes::from_static(b"xyz")),
282 Some(hook(&ended)),
283 );
284 assert!(ended.lock().unwrap().is_empty(), "fires when consumed");
285 assert_eq!(collect(body), vec![Ok(3)]);
286 assert_eq!(*ended.lock().unwrap(), [(3, None)]);
287 assert!(matches!(
288 with_hook(HttpBody::Empty, Some(hook(&ended))),
289 HttpBody::Empty
290 ));
291 assert_eq!(ended.lock().unwrap()[1], (0, None));
292 assert!(matches!(
294 with_hook(HttpBody::Bytes(Bytes::new()), None),
295 HttpBody::Bytes(_)
296 ));
297 }
298}