use core::pin::Pin;
use core::task::{Context, Poll};
use bytes::Bytes;
use futures_core::Stream;
use crate::store::StoreError;
use crate::{BoxStream, Code, ServerError};
pub enum HttpBody {
Empty,
Bytes(Bytes),
Stream {
len: u64,
stream: BoxStream<'static, Result<Bytes, ServerError>>,
},
}
impl core::fmt::Debug for HttpBody {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Empty => f.write_str("Empty"),
Self::Bytes(b) => f.debug_tuple("Bytes").field(&b.len()).finish(),
Self::Stream { len, .. } => f.debug_struct("Stream").field("len", len).finish(),
}
}
}
#[cfg(not(target_arch = "wasm32"))]
pub type EndHook = Box<dyn FnOnce(u64, Result<(), &ServerError>) + Send>;
#[cfg(target_arch = "wasm32")]
pub type EndHook = Box<dyn FnOnce(u64, Result<(), &ServerError>)>;
fn overrun() -> ServerError {
ServerError::unavailable("object body length changed")
}
struct Exact {
inner: BoxStream<'static, Result<Bytes, ServerError>>,
remaining: u64,
sent: u64,
hook: Option<EndHook>,
done: bool,
}
impl Exact {
fn end(&mut self, result: Result<(), &ServerError>) {
self.done = true;
if let Some(hook) = self.hook.take() {
hook(self.sent, result);
}
}
fn fail(&mut self, error: ServerError) -> Poll<Option<Result<Bytes, ServerError>>> {
self.end(Err(&error));
Poll::Ready(Some(Err(error)))
}
}
impl Stream for Exact {
type Item = Result<Bytes, ServerError>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
if self.done {
return Poll::Ready(None);
}
match self.inner.as_mut().poll_next(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(Some(Ok(piece))) => {
let n = piece.len() as u64;
if n > self.remaining {
return self.fail(overrun());
}
self.remaining -= n;
self.sent += n;
Poll::Ready(Some(Ok(piece)))
}
Poll::Ready(Some(Err(error))) => self.fail(error),
Poll::Ready(None) if self.remaining != 0 => self.fail(overrun()),
Poll::Ready(None) => {
self.end(Ok(()));
Poll::Ready(None)
}
}
}
}
impl Drop for Exact {
fn drop(&mut self) {
if !self.done {
self.end(Err(&ServerError::new(Code::Canceled, "request canceled")));
}
}
}
fn wrap(
stream: BoxStream<'static, Result<Bytes, ServerError>>,
len: u64,
hook: Option<EndHook>,
) -> HttpBody {
HttpBody::Stream {
len,
stream: Box::pin(Exact {
inner: stream,
remaining: len,
sent: 0,
hook,
done: false,
}),
}
}
#[must_use]
pub fn exact(
stream: BoxStream<'static, Result<Bytes, StoreError>>,
len: u64,
hook: Option<EndHook>,
) -> HttpBody {
let stream = futures::StreamExt::map(stream, |piece| {
piece.map_err(|error| {
tracing::warn!(detail = %error, "object body read failed");
ServerError::unavailable("object storage request failed")
})
});
wrap(Box::pin(stream), len, hook)
}
#[must_use]
pub fn with_hook(body: HttpBody, hook: Option<EndHook>) -> HttpBody {
let Some(hook) = hook else {
return body;
};
match body {
HttpBody::Empty => {
hook(0, Ok(()));
HttpBody::Empty
}
HttpBody::Bytes(bytes) => {
let len = bytes.len() as u64;
wrap(
Box::pin(futures::stream::once(core::future::ready(Ok(bytes)))),
len,
Some(hook),
)
}
HttpBody::Stream { len, stream } => wrap(stream, len, Some(hook)),
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use std::sync::{Arc, Mutex};
use futures::StreamExt as _;
use futures_executor::block_on;
use super::*;
type Ended = Arc<Mutex<Vec<(u64, Option<Code>)>>>;
fn hook(ended: &Ended) -> EndHook {
let ended = ended.clone();
Box::new(move |sent, result| {
ended
.lock()
.unwrap()
.push((sent, result.err().map(ServerError::code)));
})
}
fn source(
pieces: Vec<Result<&'static [u8], StoreError>>,
) -> BoxStream<'static, Result<Bytes, StoreError>> {
Box::pin(futures::stream::iter(
pieces
.into_iter()
.map(|piece| piece.map(Bytes::from_static))
.collect::<Vec<_>>(),
))
}
fn collect(body: HttpBody) -> Vec<Result<usize, Code>> {
let HttpBody::Stream { stream, .. } = body else {
panic!("expected a stream");
};
block_on(
stream
.map(|p| p.map(|b| b.len()).map_err(|e| e.code()))
.collect(),
)
}
#[test]
fn an_exact_body_reports_its_end_once() {
let ended = Ended::default();
let body = exact(source(vec![Ok(b"abc"), Ok(b"de")]), 5, Some(hook(&ended)));
assert_eq!(collect(body), vec![Ok(3), Ok(2)]);
assert_eq!(*ended.lock().unwrap(), [(5, None)]);
}
#[test]
fn a_wrong_length_or_a_backend_failure_fails_the_body_and_the_hook() {
for (pieces, len, sent, want) in [
(vec![Ok(&b"abcd"[..])], 3, 0, vec![Err(Code::Unavailable)]),
(
vec![Ok(&b"ab"[..])],
3,
2,
vec![Ok(2), Err(Code::Unavailable)],
),
(
vec![
Ok(&b"ab"[..]),
Err(StoreError::unavailable("SECRET backend detail")),
],
4,
2,
vec![Ok(2), Err(Code::Unavailable)],
),
] {
let ended = Ended::default();
let got = collect(exact(source(pieces), len, Some(hook(&ended))));
assert_eq!(got, want);
assert_eq!(*ended.lock().unwrap(), [(sent, Some(Code::Unavailable))]);
}
}
#[test]
fn a_dropped_body_reports_canceled_with_what_was_sent() {
let ended = Ended::default();
let HttpBody::Stream { mut stream, .. } =
exact(source(vec![Ok(b"abc"), Ok(b"def")]), 6, Some(hook(&ended)))
else {
panic!();
};
assert_eq!(block_on(stream.next()).unwrap().unwrap().len(), 3);
drop(stream);
assert_eq!(*ended.lock().unwrap(), [(3, Some(Code::Canceled))]);
}
#[test]
fn a_backend_error_is_redacted_to_a_fixed_message() {
let body = exact(
source(vec![Err(StoreError::unavailable("SECRET backend detail"))]),
1,
None,
);
let HttpBody::Stream { mut stream, .. } = body else {
panic!();
};
let error = block_on(stream.next()).unwrap().unwrap_err();
assert!(!format!("{error:?} {error}").contains("SECRET"));
assert_eq!(error.public_message(), "object storage request failed");
}
#[test]
fn a_hook_also_covers_in_memory_and_empty_bodies() {
let ended = Ended::default();
let body = with_hook(
HttpBody::Bytes(Bytes::from_static(b"xyz")),
Some(hook(&ended)),
);
assert!(ended.lock().unwrap().is_empty(), "fires when consumed");
assert_eq!(collect(body), vec![Ok(3)]);
assert_eq!(*ended.lock().unwrap(), [(3, None)]);
assert!(matches!(
with_hook(HttpBody::Empty, Some(hook(&ended))),
HttpBody::Empty
));
assert_eq!(ended.lock().unwrap()[1], (0, None));
assert!(matches!(
with_hook(HttpBody::Bytes(Bytes::new()), None),
HttpBody::Bytes(_)
));
}
}