Skip to main content

rama_http/layer/util/
compression.rs

1//! Types used by compression and decompression middleware.
2
3#![expect(
4    clippy::allow_attributes,
5    reason = "macro-generated `#[allow]` attributes whose underlying lints fire only for some expansions"
6)]
7#![expect(
8    clippy::unreachable,
9    reason = "SENTINEL_ERROR_CODE is only stored alongside an underlying body error, never observed at the unreachable branch"
10)]
11
12use crate::body::{Frame, SizeHint, StreamingBody};
13use pin_project_lite::pin_project;
14use rama_core::bytes::{Buf, Bytes, BytesMut};
15use rama_core::error::BoxError;
16use rama_core::futures::Stream;
17use rama_core::futures::ready;
18use rama_core::stream::io::StreamReader;
19use std::io::ErrorKind;
20use std::{
21    io,
22    pin::Pin,
23    task::{Context, Poll},
24};
25use tokio::io::AsyncRead;
26
27/// The `StreamingBody::poll_frame` body shared verbatim by the compression and
28/// decompression `BodyInner` enums: forward the four codec variants and rechunk
29/// the `Identity` passthrough into `Bytes`.
30///
31/// `BodyInnerProj`, `ready!`, `Frame`, `Buf` and `Poll` resolve at the call
32/// site — both `body.rs` modules import the same set.
33macro_rules! compressed_body_poll_frame {
34    ($self:expr, $cx:expr) => {
35        match $self.project().inner.project() {
36            BodyInnerProj::Gzip { inner } => inner.poll_frame($cx),
37            BodyInnerProj::Deflate { inner } => inner.poll_frame($cx),
38            BodyInnerProj::Brotli { inner } => inner.poll_frame($cx),
39            BodyInnerProj::Zstd { inner } => inner.poll_frame($cx),
40            BodyInnerProj::Identity { inner } => match ready!(inner.poll_frame($cx)) {
41                Some(Ok(frame)) => {
42                    let frame = frame.map_data(|mut buf| buf.copy_to_bytes(buf.remaining()));
43                    Poll::Ready(Some(Ok(frame)))
44                }
45                Some(Err(err)) => Poll::Ready(Some(Err(err.into()))),
46                None => Poll::Ready(None),
47            },
48        }
49    };
50}
51pub(crate) use compressed_body_poll_frame;
52
53/// Generate a [`DecorateAsyncRead`] impl for a codec type. The four codec
54/// `Input`/`Output` aliases and the `get_pin_mut` delegation are identical
55/// across every (de)compression codec; only the `apply` body differs, so it is
56/// supplied as a closure-shaped block.
57macro_rules! impl_decorate_async_read {
58    ($codec:ident: |$input:pat_param, $quality:pat_param| $apply:block) => {
59        impl<B> DecorateAsyncRead for $codec<B>
60        where
61            B: StreamingBody,
62        {
63            type Input = AsyncReadBody<B>;
64            type Output = $codec<Self::Input>;
65
66            fn apply($input: Self::Input, $quality: CompressionLevel) -> Self::Output $apply
67
68            fn get_pin_mut(pinned: Pin<&mut Self::Output>) -> Pin<&mut Self::Input> {
69                pinned.get_pin_mut()
70            }
71        }
72    };
73}
74pub(crate) use impl_decorate_async_read;
75
76/// A `Body` that has been converted into an `AsyncRead`.
77pub(crate) type AsyncReadBody<B> = StreamReader<
78    StreamErrorIntoIoError<BodyIntoStream<B>, <B as StreamingBody>::Error>,
79    <B as StreamingBody>::Data,
80>;
81
82/// Trait for applying some decorator to an `AsyncRead`
83pub(crate) trait DecorateAsyncRead {
84    type Input: AsyncRead;
85    type Output: AsyncRead;
86
87    /// Apply the decorator
88    fn apply(input: Self::Input, quality: CompressionLevel) -> Self::Output;
89
90    /// Get a pinned mutable reference to the original input.
91    ///
92    /// This is necessary to implement `Body::poll_trailers`.
93    fn get_pin_mut(pinned: Pin<&mut Self::Output>) -> Pin<&mut Self::Input>;
94}
95
96pin_project! {
97    /// `Body` that has been decorated by an `AsyncRead`
98    pub(crate) struct WrapBody<M: DecorateAsyncRead> {
99        #[pin]
100        // rust-analyser thinks this field is private if its `pub(crate)` but works fine when its
101        // `pub`
102        pub read: M::Output,
103        // A buffer to temporarily store the data read from the underlying body.
104        // Reused as much as possible to optimize allocations.
105        buf: BytesMut,
106        read_all_data: bool,
107        // When set, a mid-stream decode error (after some data was decoded) ends
108        // the body cleanly instead of erroring. Opt-in, response-decompression
109        // only — see `with_tolerate_decode_errors`.
110        tolerate_decode_errors: bool,
111    }
112}
113
114impl<M: DecorateAsyncRead> WrapBody<M> {
115    const INTERNAL_BUF_CAPACITY: usize = 8096;
116}
117
118impl<M: DecorateAsyncRead> WrapBody<M> {
119    #[allow(dead_code)]
120    pub(crate) fn new<B>(body: B, quality: CompressionLevel) -> Self
121    where
122        B: StreamingBody,
123        M: DecorateAsyncRead<Input = AsyncReadBody<B>>,
124    {
125        // convert `Body` into a `Stream`
126        let stream = BodyIntoStream::new(body);
127
128        // an adapter that converts the error type into `io::Error` while storing the actual error
129        // `StreamReader` requires the error type is `io::Error`
130        let stream = StreamErrorIntoIoError::<_, B::Error>::new(stream);
131
132        // convert `Stream` into an `AsyncRead`
133        let read = StreamReader::new(stream);
134
135        // apply decorator to `AsyncRead` yielding another `AsyncRead`
136        let read = M::apply(read, quality);
137
138        Self {
139            read,
140            buf: BytesMut::with_capacity(Self::INTERNAL_BUF_CAPACITY),
141            read_all_data: false,
142            tolerate_decode_errors: false,
143        }
144    }
145
146    /// Opt in to ending the body cleanly on a mid-stream decode error (after at
147    /// least one decoded chunk) instead of surfacing the error.
148    ///
149    /// Used only for RESPONSE decompression where a downstream consumer re-encodes
150    /// and relays the decoded stream (e.g. the MITM HTML-rewrite proxy): a truncated
151    /// upstream body then yields a short-but-well-formed stream to the client rather
152    /// than aborting it. Off by default; genuine upstream transport errors still
153    /// propagate, and request-body decompression stays strict.
154    pub(crate) fn with_tolerate_decode_errors(mut self, tolerate: bool) -> Self {
155        self.tolerate_decode_errors = tolerate;
156        self
157    }
158}
159
160impl<B, M> StreamingBody for WrapBody<M>
161where
162    B: StreamingBody<Error: Into<BoxError>>,
163    M: DecorateAsyncRead<Input = AsyncReadBody<B>>,
164{
165    type Data = Bytes;
166    type Error = BoxError;
167
168    fn poll_frame(
169        self: Pin<&mut Self>,
170        cx: &mut Context<'_>,
171    ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
172        let mut this = self.project();
173
174        if !*this.read_all_data {
175            if this.buf.capacity() == 0 {
176                this.buf.reserve(Self::INTERNAL_BUF_CAPACITY);
177            }
178
179            let result =
180                rama_core::stream::io::poll_read_buf(this.read.as_mut(), cx, &mut this.buf);
181
182            match ready!(result) {
183                Ok(0) => {
184                    *this.read_all_data = true;
185                }
186                Ok(_) => {
187                    let chunk = this.buf.split().freeze();
188                    return Poll::Ready(Some(Ok(Frame::data(chunk))));
189                }
190                Err(err) => {
191                    let body_error: Option<B::Error> = M::get_pin_mut(this.read.as_mut())
192                        .get_pin_mut()
193                        .project()
194                        .error
195                        .take();
196
197                    let read_some_data = M::get_pin_mut(this.read.as_mut())
198                        .get_pin_mut()
199                        .project()
200                        .read_some_data;
201
202                    if let Some(body_error) = body_error {
203                        return Poll::Ready(Some(Err(body_error.into())));
204                    } else if err.raw_os_error() == Some(SENTINEL_ERROR_CODE) {
205                        // SENTINEL_ERROR_CODE only gets used when storing
206                        // an underlying body error
207                        unreachable!()
208                    } else if *read_some_data {
209                        if err.kind() == ErrorKind::UnexpectedEof
210                            && M::get_pin_mut(this.read.as_mut())
211                                .get_pin_mut()
212                                .inner
213                                .yielded_all_data
214                        {
215                            *this.read_all_data = true;
216                        } else if *this.tolerate_decode_errors {
217                            // Opt-in: a mid-stream decode error (e.g. a corrupt /
218                            // truncated upstream body) ends the decoded stream
219                            // cleanly instead of erroring. Already-decoded frames
220                            // were emitted earlier; `buf` is empty at this point (it
221                            // is split on every Ok and not advanced on Err), so we
222                            // lose nothing by ending now. We return None directly
223                            // rather than falling through to the trailer poll, which
224                            // could otherwise surface leftover undecoded source bytes
225                            // as an "extra bytes" error and re-abort the stream. Lets
226                            // a re-encoding relay deliver a short-but-well-formed
227                            // stream instead of an aborted one (RST_STREAM).
228                            rama_core::telemetry::tracing::debug!(
229                                "decompression: tolerating mid-stream decode error ({err}); ending body cleanly"
230                            );
231                            return Poll::Ready(None);
232                        } else {
233                            return Poll::Ready(Some(Err(err.into())));
234                        }
235                    }
236                }
237            }
238        }
239
240        // poll any remaining frames, such as trailers
241        let body = M::get_pin_mut(this.read).get_pin_mut().get_pin_mut();
242        match ready!(body.poll_frame(cx)) {
243            Some(Ok(frame)) if frame.is_trailers() => Poll::Ready(Some(Ok(
244                frame.map_data(|mut data| data.copy_to_bytes(data.remaining()))
245            ))),
246            Some(Ok(frame)) => match frame.into_data() {
247                // Payload after the decompressor reported end-of-stream means
248                // the body and compressed stream disagree about where the
249                // content ends.
250                Ok(data) if data.has_remaining() => Poll::Ready(Some(Err(
251                    "there are extra bytes after body has been decompressed".into(),
252                ))),
253                // An empty data frame carries nothing, but it is not the end of
254                // the body: trailers may still follow it. Ending the stream here
255                // would strand them, and `StreamingBody` forbids polling once
256                // `None` has been returned, so they could never be recovered.
257                Ok(mut data) => {
258                    Poll::Ready(Some(Ok(Frame::data(data.copy_to_bytes(data.remaining())))))
259                }
260                Err(frame) => Poll::Ready(Some(Ok(
261                    frame.map_data(|mut data| data.copy_to_bytes(data.remaining()))
262                ))),
263            },
264            Some(Err(err)) => Poll::Ready(Some(Err(err.into()))),
265            None => Poll::Ready(None),
266        }
267    }
268}
269
270pin_project! {
271    pub(crate) struct BodyIntoStream<B>
272    where
273        B: StreamingBody,
274    {
275        #[pin]
276        body: B,
277        yielded_all_data: bool,
278        non_data_frame: Option<Frame<B::Data>>,
279    }
280}
281
282#[allow(dead_code)]
283impl<B> BodyIntoStream<B>
284where
285    B: StreamingBody,
286{
287    pub(crate) fn new(body: B) -> Self {
288        Self {
289            body,
290            yielded_all_data: false,
291            non_data_frame: None,
292        }
293    }
294
295    /// Get a reference to the inner body
296    pub(crate) fn get_ref(&self) -> &B {
297        &self.body
298    }
299
300    /// Get a mutable reference to the inner body
301    pub(crate) fn get_mut(&mut self) -> &mut B {
302        &mut self.body
303    }
304
305    /// Get a pinned mutable reference to the inner body
306    pub(crate) fn get_pin_mut(self: Pin<&mut Self>) -> Pin<&mut B> {
307        self.project().body
308    }
309
310    /// Consume `self`, returning the inner body
311    pub(crate) fn into_inner(self) -> B {
312        self.body
313    }
314}
315
316impl<B> Stream for BodyIntoStream<B>
317where
318    B: StreamingBody,
319{
320    type Item = Result<B::Data, B::Error>;
321
322    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
323        loop {
324            let this = self.as_mut().project();
325
326            if *this.yielded_all_data {
327                return Poll::Ready(None);
328            }
329
330            match std::task::ready!(this.body.poll_frame(cx)) {
331                Some(Ok(frame)) => match frame.into_data() {
332                    Ok(data) => return Poll::Ready(Some(Ok(data))),
333                    Err(frame) => {
334                        *this.yielded_all_data = true;
335                        *this.non_data_frame = Some(frame);
336                    }
337                },
338                Some(Err(err)) => return Poll::Ready(Some(Err(err))),
339                None => {
340                    *this.yielded_all_data = true;
341                }
342            }
343        }
344    }
345}
346
347impl<B> StreamingBody for BodyIntoStream<B>
348where
349    B: StreamingBody,
350{
351    type Data = B::Data;
352    type Error = B::Error;
353
354    fn poll_frame(
355        mut self: Pin<&mut Self>,
356        cx: &mut Context<'_>,
357    ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
358        // First drive the stream impl. This consumes all data frames and buffer at most one
359        // trailers frame.
360        if let Some(frame) = std::task::ready!(self.as_mut().poll_next(cx)) {
361            return Poll::Ready(Some(frame.map(Frame::data)));
362        }
363
364        let this = self.project();
365
366        // Yield the trailers frame `poll_next` hit.
367        if let Some(frame) = this.non_data_frame.take() {
368            return Poll::Ready(Some(Ok(frame)));
369        }
370
371        // Yield any remaining frames in the body. There shouldn't be any after the trailers but
372        // you never know.
373        this.body.poll_frame(cx)
374    }
375
376    #[inline]
377    fn size_hint(&self) -> SizeHint {
378        self.body.size_hint()
379    }
380}
381
382pin_project! {
383    pub(crate) struct StreamErrorIntoIoError<S, E> {
384        #[pin]
385        inner: S,
386        error: Option<E>,
387        read_some_data: bool,
388    }
389}
390
391impl<S, E> StreamErrorIntoIoError<S, E> {
392    pub(crate) fn new(inner: S) -> Self {
393        Self {
394            inner,
395            error: None,
396            read_some_data: false,
397        }
398    }
399
400    /// Get a pinned mutable reference to the inner inner
401    pub(crate) fn get_pin_mut(self: Pin<&mut Self>) -> Pin<&mut S> {
402        self.project().inner
403    }
404}
405
406impl<S, T, E> Stream for StreamErrorIntoIoError<S, E>
407where
408    S: Stream<Item = Result<T, E>>,
409{
410    type Item = Result<T, io::Error>;
411
412    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
413        let this = self.project();
414        match ready!(this.inner.poll_next(cx)) {
415            None => Poll::Ready(None),
416            Some(Ok(value)) => {
417                *this.read_some_data = true;
418                Poll::Ready(Some(Ok(value)))
419            }
420            Some(Err(err)) => {
421                *this.error = Some(err);
422                Poll::Ready(Some(Err(io::Error::from_raw_os_error(SENTINEL_ERROR_CODE))))
423            }
424        }
425    }
426}
427
428pub(crate) const SENTINEL_ERROR_CODE: i32 = -837459418;
429
430/// Level of compression data should be compressed with.
431#[non_exhaustive]
432#[derive(Default, Clone, Copy, Debug, Eq, PartialEq, Hash)]
433pub enum CompressionLevel {
434    /// Fastest quality of compression, usually produces bigger size.
435    Fastest,
436    /// Best quality of compression, usually produces the smallest size.
437    Best,
438    /// Default quality of compression defined by the selected compression
439    /// algorithm.
440    #[default]
441    Default,
442    /// Precise quality based on the underlying compression algorithms'
443    /// qualities.
444    ///
445    /// The interpretation of this depends on the algorithm chosen and the
446    /// specific implementation backing it.
447    ///
448    /// Qualities are implicitly clamped to the algorithm's maximum.
449    Precise(u32),
450}
451
452use async_compression::Level as AsyncCompressionLevel;
453use compression_core::Level as CompressionCoreLevel;
454
455impl CompressionLevel {
456    #[allow(dead_code)]
457    pub(crate) fn into_async_compression(self) -> AsyncCompressionLevel {
458        match self {
459            Self::Fastest => AsyncCompressionLevel::Fastest,
460            Self::Best => AsyncCompressionLevel::Best,
461            Self::Default => AsyncCompressionLevel::Default,
462            Self::Precise(quality) => {
463                AsyncCompressionLevel::Precise(quality.try_into().unwrap_or(i32::MAX))
464            }
465        }
466    }
467}
468
469impl CompressionLevel {
470    #[allow(dead_code)]
471    pub(crate) fn into_compression_core(self) -> CompressionCoreLevel {
472        match self {
473            Self::Fastest => CompressionCoreLevel::Fastest,
474            Self::Best => CompressionCoreLevel::Best,
475            Self::Default => CompressionCoreLevel::Default,
476            Self::Precise(quality) => {
477                CompressionCoreLevel::Precise(quality.try_into().unwrap_or(i32::MAX))
478            }
479        }
480    }
481}