use std::fmt;
use std::pin::Pin;
use std::task::{Context, Poll};
use axum::body::Body;
use axum::extract::FromRequestParts;
use axum::http::Method;
use axum::http::request::Parts;
use axum::response::{IntoResponse, Response};
use bytes::Bytes;
use futures_core::Stream;
use crate::problem::Problem;
use crate::scope::Scope;
#[allow(clippy::disallowed_types)]
type Slot =
std::sync::Arc<std::sync::Mutex<Option<BoxStream<'static, Result<Bytes, StreamError>>>>>;
#[allow(clippy::disallowed_types)]
fn slot(inner: Option<BoxStream<'static, Result<Bytes, StreamError>>>) -> Slot {
std::sync::Arc::new(std::sync::Mutex::new(inner))
}
pub type BoxStream<'a, T> = Pin<Box<dyn Stream<Item = T> + Send + 'a>>;
#[derive(Debug, Clone)]
pub struct StreamRoute {
pub method: Method,
pub path: &'static str,
pub max_bytes: usize,
}
impl StreamRoute {
#[must_use]
pub const fn post(path: &'static str, max_bytes: usize) -> Self {
Self {
method: Method::POST,
path,
max_bytes,
}
}
#[must_use]
pub const fn put(path: &'static str, max_bytes: usize) -> Self {
Self {
method: Method::PUT,
path,
max_bytes,
}
}
#[must_use]
pub const fn get(path: &'static str, max_bytes: usize) -> Self {
Self {
method: Method::GET,
path,
max_bytes,
}
}
}
#[derive(Debug, Clone)]
pub enum StreamError {
TooLarge,
Transport(String),
}
impl fmt::Display for StreamError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TooLarge => f.write_str("streamed body exceeded its route ceiling"),
Self::Transport(detail) => write!(f, "stream transport error: {detail}"),
}
}
}
impl std::error::Error for StreamError {}
impl From<StreamError> for Problem {
fn from(error: StreamError) -> Self {
match error {
StreamError::TooLarge => Self::request_too_large(),
StreamError::Transport(detail) => {
tracing::error!(error = %detail, "stream transport error mapped to internal problem");
crate::logging::forward_internal_error(&format!(
"stream transport error: {detail}"
));
Self::internal()
}
}
}
}
#[derive(Clone)]
pub struct RequestStream {
slot: Slot,
}
impl RequestStream {
#[must_use]
pub fn new<S, O, E>(stream: S, max_bytes: usize) -> Self
where
S: Stream<Item = Result<O, E>> + Send + 'static,
O: Into<Bytes> + 'static,
E: fmt::Display + 'static,
{
let capped: BoxStream<'static, Result<Bytes, StreamError>> = Box::pin(Capped {
inner: Some(map_stream(stream)),
delivered: 0,
max: max_bytes,
});
Self {
slot: slot(Some(capped)),
}
}
#[must_use]
pub fn empty() -> Self {
Self { slot: slot(None) }
}
pub async fn next_chunk(&mut self) -> Option<Result<Bytes, StreamError>> {
std::future::poll_fn(|cx| Pin::new(&mut *self).poll_next(cx)).await
}
}
impl Stream for RequestStream {
type Item = Result<Bytes, StreamError>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
let mut slot = this.slot.lock().expect("stream slot lock uncontended");
match slot.as_mut() {
Some(stream) => stream.as_mut().poll_next(cx),
None => Poll::Ready(None),
}
}
}
impl<S: Send + Sync> FromRequestParts<S> for RequestStream {
type Rejection = Problem;
#[allow(clippy::unused_async_trait_impl)]
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
if let Some(stream) = parts.extensions.remove::<RequestStream>() {
return Ok(stream);
}
let mut problem = Problem::internal().with_detail(
"RequestStream is only available on a route declared in the owning module's \
streaming_routes()",
);
if let Some(scope) = parts.extensions.get::<Scope>() {
problem = problem.instance(&scope.request_id);
}
Err(problem)
}
}
#[derive(Clone)]
pub struct ResponseStream {
slot: Slot,
}
impl ResponseStream {
#[must_use]
pub fn new<S, O, E>(stream: S) -> Self
where
S: Stream<Item = Result<O, E>> + Send + 'static,
O: Into<Bytes> + 'static,
E: fmt::Display + 'static,
{
Self {
slot: slot(Some(map_stream(stream))),
}
}
#[must_use]
pub fn take(&self) -> Option<BoxStream<'static, Result<Bytes, StreamError>>> {
self.slot
.lock()
.expect("stream slot lock uncontended")
.take()
}
}
impl IntoResponse for ResponseStream {
fn into_response(self) -> Response {
let body = Body::from_stream(BodySource {
slot: std::sync::Arc::clone(&self.slot),
inner: None,
});
let mut response = Response::new(body);
response.extensions_mut().insert(self);
response
}
}
struct BodySource {
slot: Slot,
inner: Option<BoxStream<'static, Result<Bytes, StreamError>>>,
}
impl Stream for BodySource {
type Item = Result<Bytes, StreamError>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
if this.inner.is_none() {
this.inner = this
.slot
.lock()
.expect("stream slot lock uncontended")
.take();
}
match this.inner.as_mut() {
Some(stream) => stream.as_mut().poll_next(cx),
None => Poll::Ready(None),
}
}
}
struct Capped {
inner: Option<BoxStream<'static, Result<Bytes, StreamError>>>,
delivered: usize,
max: usize,
}
impl Stream for Capped {
type Item = Result<Bytes, StreamError>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
let polled = match this.inner.as_mut() {
Some(stream) => stream.as_mut().poll_next(cx),
None => return Poll::Ready(None),
};
match polled {
Poll::Ready(Some(Ok(chunk))) => {
if this.delivered.saturating_add(chunk.len()) > this.max {
this.inner = None;
Poll::Ready(Some(Err(StreamError::TooLarge)))
} else {
this.delivered += chunk.len();
Poll::Ready(Some(Ok(chunk)))
}
}
Poll::Ready(Some(Err(err))) => Poll::Ready(Some(Err(err))),
Poll::Ready(None) => {
this.inner = None;
Poll::Ready(None)
}
Poll::Pending => Poll::Pending,
}
}
}
fn map_stream<S, O, E>(stream: S) -> BoxStream<'static, Result<Bytes, StreamError>>
where
S: Stream<Item = Result<O, E>> + Send + 'static,
O: Into<Bytes> + 'static,
E: fmt::Display + 'static,
{
Box::pin(MapStream {
inner: Box::pin(stream),
})
}
struct MapStream<T, E> {
inner: Pin<Box<dyn Stream<Item = Result<T, E>> + Send + 'static>>,
}
impl<T, E> Stream for MapStream<T, E>
where
T: Into<Bytes> + 'static,
E: fmt::Display + 'static,
{
type Item = Result<Bytes, StreamError>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
match this.inner.as_mut().poll_next(cx) {
Poll::Ready(Some(Ok(item))) => Poll::Ready(Some(Ok(item.into()))),
Poll::Ready(Some(Err(err))) => {
Poll::Ready(Some(Err(StreamError::Transport(err.to_string()))))
}
Poll::Ready(None) => Poll::Ready(None),
Poll::Pending => Poll::Pending,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct VecStream<T> {
items: std::vec::IntoIter<T>,
}
impl<T: std::marker::Unpin> Stream for VecStream<T> {
type Item = T;
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<T>> {
Poll::Ready(self.get_mut().items.next())
}
}
fn chunks(
items: Vec<Result<&'static [u8], &'static str>>,
) -> impl Stream<Item = Result<Vec<u8>, String>> + Send {
VecStream {
items: items
.into_iter()
.map(|item| item.map(<[u8]>::to_vec).map_err(str::to_owned))
.collect::<Vec<_>>()
.into_iter(),
}
}
#[pollster::test]
async fn the_crossing_chunk_is_refused_and_the_stream_fuses() {
let mut stream = RequestStream::new(
chunks(vec![Ok(b"12345"), Ok(b"67890"), Ok(b"unreachable")]),
8,
);
assert_eq!(stream.next_chunk().await.unwrap().unwrap(), "12345");
assert!(matches!(
stream.next_chunk().await.unwrap(),
Err(StreamError::TooLarge)
));
assert!(stream.next_chunk().await.is_none(), "fused after refusal");
}
#[pollster::test]
async fn a_body_exactly_at_the_ceiling_is_allowed() {
let mut stream = RequestStream::new(chunks(vec![Ok(b"1234"), Ok(b"5678")]), 8);
assert_eq!(stream.next_chunk().await.unwrap().unwrap(), "1234");
assert_eq!(stream.next_chunk().await.unwrap().unwrap(), "5678");
assert!(
stream.next_chunk().await.is_none(),
"clean end at the limit"
);
}
#[pollster::test]
async fn a_transport_error_is_mapped_not_swallowed() {
let mut stream = RequestStream::new(chunks(vec![Ok(b"hi"), Err("boom")]), 1024);
assert_eq!(stream.next_chunk().await.unwrap().unwrap(), "hi");
match stream.next_chunk().await.unwrap() {
Err(StreamError::Transport(detail)) => assert!(detail.contains("boom"), "{detail}"),
other => panic!("expected a transport error, got {other:?}"),
}
}
#[pollster::test]
async fn an_empty_stream_ends_at_once() {
let mut stream = RequestStream::empty();
assert!(stream.next_chunk().await.is_none());
}
#[pollster::test]
async fn all_response_chunks_reach_the_body() {
let response =
ResponseStream::new(chunks(vec![Ok(b"one"), Ok(b"two"), Ok(b"three")])).into_response();
let body = axum::body::to_bytes(response.into_parts().1, 1024)
.await
.expect("body reads");
assert_eq!(&body[..], b"onetwothree");
}
#[pollster::test]
async fn take_after_the_body_was_consumed_answers_none() {
let stream = ResponseStream::new(chunks(vec![Ok(b"one")]));
let response = stream.clone().into_response();
let body = axum::body::to_bytes(response.into_parts().1, 1024)
.await
.expect("body reads");
assert_eq!(&body[..], b"one");
assert!(stream.take().is_none(), "the body already took the stream");
}
#[pollster::test]
async fn take_first_leaves_the_axum_body_empty() {
let stream = ResponseStream::new(chunks(vec![Ok(b"one")]));
assert!(stream.take().is_some(), "the runtime takes the stream");
let response = stream.into_response();
let body = axum::body::to_bytes(response.into_parts().1, 1024)
.await
.expect("body reads");
assert!(body.is_empty(), "whoever takes first wins");
}
}