use crate::{Body, Headers, body::BodyType};
use futures_lite::AsyncRead;
use std::{
any::Any,
io, mem,
panic::{AssertUnwindSafe, catch_unwind},
pin::Pin,
task::Poll,
};
#[derive(Debug)]
pub(crate) struct H2Body {
body: BodyType,
len: Option<u64>,
panicked: bool,
}
impl H2Body {
pub(crate) fn new(body: Body) -> Self {
Self {
len: body.len(),
body: body.0,
panicked: false,
}
}
pub(crate) fn len(&self) -> Option<u64> {
self.len
}
pub(crate) fn trailers(&mut self) -> io::Result<Option<Headers>> {
match &mut self.body {
BodyType::Streaming {
async_read, done, ..
} if *done => contain_panic(|| async_read.get_mut().as_mut().trailers()),
_ => Ok(None),
}
}
}
impl Drop for H2Body {
fn drop(&mut self) {
let body = mem::replace(&mut self.body, BodyType::Empty);
let _ = contain_panic(|| drop(body));
}
}
fn contain_panic<T>(f: impl FnOnce() -> T) -> io::Result<T> {
catch_unwind(AssertUnwindSafe(f)).map_err(|payload| {
let message = panic_message(&*payload);
log::error!("h2 body panicked: {message}");
io::Error::other(format!("h2 body panicked: {message}"))
})
}
fn panic_message(payload: &(dyn Any + Send)) -> &str {
payload
.downcast_ref::<&str>()
.copied()
.or_else(|| payload.downcast_ref::<String>().map(String::as_str))
.unwrap_or("non-string panic payload")
}
impl AsyncRead for H2Body {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut [u8],
) -> Poll<io::Result<usize>> {
let this = self.get_mut();
if this.panicked {
return Poll::Ready(Err(io::Error::other("h2 body panicked earlier")));
}
match &mut this.body {
BodyType::Empty => Poll::Ready(Ok(0)),
BodyType::Static { content, cursor } => {
let remaining = content.len() - *cursor;
if remaining == 0 {
return Poll::Ready(Ok(0));
}
let bytes = remaining.min(buf.len());
buf[..bytes].copy_from_slice(&content[*cursor..*cursor + bytes]);
*cursor += bytes;
Poll::Ready(Ok(bytes))
}
BodyType::Streaming {
async_read,
done,
progress,
..
} => {
if *done {
return Poll::Ready(Ok(0));
}
let polled = contain_panic(|| async_read.get_mut().as_mut().poll_read(cx, buf))
.inspect_err(|_| this.panicked = true)?;
let bytes = futures_lite::ready!(polled)?;
if bytes == 0 {
*done = true;
} else {
*progress += bytes as u64;
}
Poll::Ready(Ok(bytes))
}
}
}
}