1use std::fmt;
29use std::pin::Pin;
30use std::task::{Context, Poll};
31
32use axum::body::Body;
33use axum::extract::FromRequestParts;
34use axum::http::Method;
35use axum::http::request::Parts;
36use axum::response::{IntoResponse, Response};
37use bytes::Bytes;
38use futures_core::Stream;
39
40use crate::problem::Problem;
41use crate::scope::Scope;
42
43#[allow(clippy::disallowed_types)]
50type Slot =
51 std::sync::Arc<std::sync::Mutex<Option<BoxStream<'static, Result<Bytes, StreamError>>>>>;
52
53#[allow(clippy::disallowed_types)]
57fn slot(inner: Option<BoxStream<'static, Result<Bytes, StreamError>>>) -> Slot {
58 std::sync::Arc::new(std::sync::Mutex::new(inner))
59}
60
61pub type BoxStream<'a, T> = Pin<Box<dyn Stream<Item = T> + Send + 'a>>;
66
67#[derive(Debug, Clone)]
82pub struct StreamRoute {
83 pub method: Method,
84 pub path: &'static str,
85 pub max_bytes: usize,
86}
87
88impl StreamRoute {
89 #[must_use]
92 pub const fn post(path: &'static str, max_bytes: usize) -> Self {
93 Self {
94 method: Method::POST,
95 path,
96 max_bytes,
97 }
98 }
99
100 #[must_use]
102 pub const fn put(path: &'static str, max_bytes: usize) -> Self {
103 Self {
104 method: Method::PUT,
105 path,
106 max_bytes,
107 }
108 }
109
110 #[must_use]
112 pub const fn get(path: &'static str, max_bytes: usize) -> Self {
113 Self {
114 method: Method::GET,
115 path,
116 max_bytes,
117 }
118 }
119}
120
121#[derive(Debug, Clone)]
123pub enum StreamError {
124 TooLarge,
128 Transport(String),
131}
132
133impl fmt::Display for StreamError {
134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135 match self {
136 Self::TooLarge => f.write_str("streamed body exceeded its route ceiling"),
137 Self::Transport(detail) => write!(f, "stream transport error: {detail}"),
138 }
139 }
140}
141
142impl std::error::Error for StreamError {}
143
144impl From<StreamError> for Problem {
150 fn from(error: StreamError) -> Self {
151 match error {
152 StreamError::TooLarge => Self::request_too_large(),
153 StreamError::Transport(detail) => {
154 tracing::error!(error = %detail, "stream transport error mapped to internal problem");
155 crate::logging::forward_internal_error(&format!(
159 "stream transport error: {detail}"
160 ));
161 Self::internal()
162 }
163 }
164 }
165}
166
167#[derive(Clone)]
178pub struct RequestStream {
179 slot: Slot,
180}
181
182impl RequestStream {
183 #[must_use]
188 pub fn new<S, O, E>(stream: S, max_bytes: usize) -> Self
189 where
190 S: Stream<Item = Result<O, E>> + Send + 'static,
191 O: Into<Bytes> + 'static,
192 E: fmt::Display + 'static,
193 {
194 let capped: BoxStream<'static, Result<Bytes, StreamError>> = Box::pin(Capped {
195 inner: Some(map_stream(stream)),
196 delivered: 0,
197 max: max_bytes,
198 });
199 Self {
200 slot: slot(Some(capped)),
201 }
202 }
203
204 #[must_use]
207 pub fn empty() -> Self {
208 Self { slot: slot(None) }
209 }
210
211 pub async fn next_chunk(&mut self) -> Option<Result<Bytes, StreamError>> {
215 std::future::poll_fn(|cx| Pin::new(&mut *self).poll_next(cx)).await
216 }
217}
218
219impl Stream for RequestStream {
220 type Item = Result<Bytes, StreamError>;
221
222 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
223 let this = self.get_mut();
224 let mut slot = this.slot.lock().expect("stream slot lock uncontended");
225 match slot.as_mut() {
226 Some(stream) => stream.as_mut().poll_next(cx),
227 None => Poll::Ready(None),
228 }
229 }
230}
231
232impl<S: Send + Sync> FromRequestParts<S> for RequestStream {
233 type Rejection = Problem;
234
235 #[allow(clippy::unused_async_trait_impl)]
242 async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
243 if let Some(stream) = parts.extensions.remove::<RequestStream>() {
244 return Ok(stream);
245 }
246 let mut problem = Problem::internal().with_detail(
247 "RequestStream is only available on a route declared in the owning module's \
248 streaming_routes()",
249 );
250 if let Some(scope) = parts.extensions.get::<Scope>() {
251 problem = problem.instance(&scope.request_id);
252 }
253 Err(problem)
254 }
255}
256
257#[derive(Clone)]
266pub struct ResponseStream {
267 slot: Slot,
268}
269
270impl ResponseStream {
271 #[must_use]
274 pub fn new<S, O, E>(stream: S) -> Self
275 where
276 S: Stream<Item = Result<O, E>> + Send + 'static,
277 O: Into<Bytes> + 'static,
278 E: fmt::Display + 'static,
279 {
280 Self {
281 slot: slot(Some(map_stream(stream))),
282 }
283 }
284
285 #[must_use]
296 pub fn take(&self) -> Option<BoxStream<'static, Result<Bytes, StreamError>>> {
297 self.slot
298 .lock()
299 .expect("stream slot lock uncontended")
300 .take()
301 }
302}
303
304impl IntoResponse for ResponseStream {
305 fn into_response(self) -> Response {
306 let body = Body::from_stream(BodySource {
307 slot: std::sync::Arc::clone(&self.slot),
308 inner: None,
309 });
310 let mut response = Response::new(body);
311 response.extensions_mut().insert(self);
314 response
315 }
316}
317
318struct BodySource {
322 slot: Slot,
323 inner: Option<BoxStream<'static, Result<Bytes, StreamError>>>,
324}
325
326impl Stream for BodySource {
327 type Item = Result<Bytes, StreamError>;
328
329 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
330 let this = self.get_mut();
331 if this.inner.is_none() {
332 this.inner = this
333 .slot
334 .lock()
335 .expect("stream slot lock uncontended")
336 .take();
337 }
338 match this.inner.as_mut() {
339 Some(stream) => stream.as_mut().poll_next(cx),
340 None => Poll::Ready(None),
341 }
342 }
343}
344
345struct Capped {
349 inner: Option<BoxStream<'static, Result<Bytes, StreamError>>>,
350 delivered: usize,
351 max: usize,
352}
353
354impl Stream for Capped {
355 type Item = Result<Bytes, StreamError>;
356
357 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
358 let this = self.get_mut();
359 let polled = match this.inner.as_mut() {
360 Some(stream) => stream.as_mut().poll_next(cx),
361 None => return Poll::Ready(None),
362 };
363 match polled {
364 Poll::Ready(Some(Ok(chunk))) => {
365 if this.delivered.saturating_add(chunk.len()) > this.max {
366 this.inner = None;
368 Poll::Ready(Some(Err(StreamError::TooLarge)))
369 } else {
370 this.delivered += chunk.len();
371 Poll::Ready(Some(Ok(chunk)))
372 }
373 }
374 Poll::Ready(Some(Err(err))) => Poll::Ready(Some(Err(err))),
375 Poll::Ready(None) => {
376 this.inner = None;
377 Poll::Ready(None)
378 }
379 Poll::Pending => Poll::Pending,
380 }
381 }
382}
383
384fn map_stream<S, O, E>(stream: S) -> BoxStream<'static, Result<Bytes, StreamError>>
389where
390 S: Stream<Item = Result<O, E>> + Send + 'static,
391 O: Into<Bytes> + 'static,
392 E: fmt::Display + 'static,
393{
394 Box::pin(MapStream {
395 inner: Box::pin(stream),
396 })
397}
398
399struct MapStream<T, E> {
400 inner: Pin<Box<dyn Stream<Item = Result<T, E>> + Send + 'static>>,
401}
402
403impl<T, E> Stream for MapStream<T, E>
404where
405 T: Into<Bytes> + 'static,
406 E: fmt::Display + 'static,
407{
408 type Item = Result<Bytes, StreamError>;
409
410 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
411 let this = self.get_mut();
412 match this.inner.as_mut().poll_next(cx) {
413 Poll::Ready(Some(Ok(item))) => Poll::Ready(Some(Ok(item.into()))),
414 Poll::Ready(Some(Err(err))) => {
415 Poll::Ready(Some(Err(StreamError::Transport(err.to_string()))))
416 }
417 Poll::Ready(None) => Poll::Ready(None),
418 Poll::Pending => Poll::Pending,
419 }
420 }
421}
422
423#[cfg(test)]
424mod tests {
425 use super::*;
426
427 struct VecStream<T> {
430 items: std::vec::IntoIter<T>,
431 }
432
433 impl<T: std::marker::Unpin> Stream for VecStream<T> {
434 type Item = T;
435
436 fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<T>> {
437 Poll::Ready(self.get_mut().items.next())
438 }
439 }
440
441 fn chunks(
444 items: Vec<Result<&'static [u8], &'static str>>,
445 ) -> impl Stream<Item = Result<Vec<u8>, String>> + Send {
446 VecStream {
447 items: items
448 .into_iter()
449 .map(|item| item.map(<[u8]>::to_vec).map_err(str::to_owned))
450 .collect::<Vec<_>>()
451 .into_iter(),
452 }
453 }
454
455 #[pollster::test]
456 async fn the_crossing_chunk_is_refused_and_the_stream_fuses() {
457 let mut stream = RequestStream::new(
458 chunks(vec![Ok(b"12345"), Ok(b"67890"), Ok(b"unreachable")]),
459 8,
460 );
461 assert_eq!(stream.next_chunk().await.unwrap().unwrap(), "12345");
462 assert!(matches!(
464 stream.next_chunk().await.unwrap(),
465 Err(StreamError::TooLarge)
466 ));
467 assert!(stream.next_chunk().await.is_none(), "fused after refusal");
468 }
469
470 #[pollster::test]
471 async fn a_body_exactly_at_the_ceiling_is_allowed() {
472 let mut stream = RequestStream::new(chunks(vec![Ok(b"1234"), Ok(b"5678")]), 8);
473 assert_eq!(stream.next_chunk().await.unwrap().unwrap(), "1234");
474 assert_eq!(stream.next_chunk().await.unwrap().unwrap(), "5678");
475 assert!(
476 stream.next_chunk().await.is_none(),
477 "clean end at the limit"
478 );
479 }
480
481 #[pollster::test]
482 async fn a_transport_error_is_mapped_not_swallowed() {
483 let mut stream = RequestStream::new(chunks(vec![Ok(b"hi"), Err("boom")]), 1024);
484 assert_eq!(stream.next_chunk().await.unwrap().unwrap(), "hi");
485 match stream.next_chunk().await.unwrap() {
486 Err(StreamError::Transport(detail)) => assert!(detail.contains("boom"), "{detail}"),
487 other => panic!("expected a transport error, got {other:?}"),
488 }
489 }
490
491 #[pollster::test]
492 async fn an_empty_stream_ends_at_once() {
493 let mut stream = RequestStream::empty();
494 assert!(stream.next_chunk().await.is_none());
495 }
496
497 #[pollster::test]
498 async fn all_response_chunks_reach_the_body() {
499 let response =
500 ResponseStream::new(chunks(vec![Ok(b"one"), Ok(b"two"), Ok(b"three")])).into_response();
501 let body = axum::body::to_bytes(response.into_parts().1, 1024)
502 .await
503 .expect("body reads");
504 assert_eq!(&body[..], b"onetwothree");
505 }
506
507 #[pollster::test]
508 async fn take_after_the_body_was_consumed_answers_none() {
509 let stream = ResponseStream::new(chunks(vec![Ok(b"one")]));
510 let response = stream.clone().into_response();
511 let body = axum::body::to_bytes(response.into_parts().1, 1024)
512 .await
513 .expect("body reads");
514 assert_eq!(&body[..], b"one");
515 assert!(stream.take().is_none(), "the body already took the stream");
516 }
517
518 #[pollster::test]
519 async fn take_first_leaves_the_axum_body_empty() {
520 let stream = ResponseStream::new(chunks(vec![Ok(b"one")]));
521 assert!(stream.take().is_some(), "the runtime takes the stream");
522 let response = stream.into_response();
523 let body = axum::body::to_bytes(response.into_parts().1, 1024)
524 .await
525 .expect("body reads");
526 assert!(body.is_empty(), "whoever takes first wins");
527 }
528}