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)) => {
247                if let Ok(bytes) = frame.into_data()
248                    && bytes.has_remaining()
249                {
250                    return Poll::Ready(Some(Err(
251                        "there are extra bytes after body has been decompressed".into(),
252                    )));
253                }
254                Poll::Ready(None)
255            }
256            Some(Err(err)) => Poll::Ready(Some(Err(err.into()))),
257            None => Poll::Ready(None),
258        }
259    }
260}
261
262pin_project! {
263    pub(crate) struct BodyIntoStream<B>
264    where
265        B: StreamingBody,
266    {
267        #[pin]
268        body: B,
269        yielded_all_data: bool,
270        non_data_frame: Option<Frame<B::Data>>,
271    }
272}
273
274#[allow(dead_code)]
275impl<B> BodyIntoStream<B>
276where
277    B: StreamingBody,
278{
279    pub(crate) fn new(body: B) -> Self {
280        Self {
281            body,
282            yielded_all_data: false,
283            non_data_frame: None,
284        }
285    }
286
287    /// Get a reference to the inner body
288    pub(crate) fn get_ref(&self) -> &B {
289        &self.body
290    }
291
292    /// Get a mutable reference to the inner body
293    pub(crate) fn get_mut(&mut self) -> &mut B {
294        &mut self.body
295    }
296
297    /// Get a pinned mutable reference to the inner body
298    pub(crate) fn get_pin_mut(self: Pin<&mut Self>) -> Pin<&mut B> {
299        self.project().body
300    }
301
302    /// Consume `self`, returning the inner body
303    pub(crate) fn into_inner(self) -> B {
304        self.body
305    }
306}
307
308impl<B> Stream for BodyIntoStream<B>
309where
310    B: StreamingBody,
311{
312    type Item = Result<B::Data, B::Error>;
313
314    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
315        loop {
316            let this = self.as_mut().project();
317
318            if *this.yielded_all_data {
319                return Poll::Ready(None);
320            }
321
322            match std::task::ready!(this.body.poll_frame(cx)) {
323                Some(Ok(frame)) => match frame.into_data() {
324                    Ok(data) => return Poll::Ready(Some(Ok(data))),
325                    Err(frame) => {
326                        *this.yielded_all_data = true;
327                        *this.non_data_frame = Some(frame);
328                    }
329                },
330                Some(Err(err)) => return Poll::Ready(Some(Err(err))),
331                None => {
332                    *this.yielded_all_data = true;
333                }
334            }
335        }
336    }
337}
338
339impl<B> StreamingBody for BodyIntoStream<B>
340where
341    B: StreamingBody,
342{
343    type Data = B::Data;
344    type Error = B::Error;
345
346    fn poll_frame(
347        mut self: Pin<&mut Self>,
348        cx: &mut Context<'_>,
349    ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
350        // First drive the stream impl. This consumes all data frames and buffer at most one
351        // trailers frame.
352        if let Some(frame) = std::task::ready!(self.as_mut().poll_next(cx)) {
353            return Poll::Ready(Some(frame.map(Frame::data)));
354        }
355
356        let this = self.project();
357
358        // Yield the trailers frame `poll_next` hit.
359        if let Some(frame) = this.non_data_frame.take() {
360            return Poll::Ready(Some(Ok(frame)));
361        }
362
363        // Yield any remaining frames in the body. There shouldn't be any after the trailers but
364        // you never know.
365        this.body.poll_frame(cx)
366    }
367
368    #[inline]
369    fn size_hint(&self) -> SizeHint {
370        self.body.size_hint()
371    }
372}
373
374pin_project! {
375    pub(crate) struct StreamErrorIntoIoError<S, E> {
376        #[pin]
377        inner: S,
378        error: Option<E>,
379        read_some_data: bool,
380    }
381}
382
383impl<S, E> StreamErrorIntoIoError<S, E> {
384    pub(crate) fn new(inner: S) -> Self {
385        Self {
386            inner,
387            error: None,
388            read_some_data: false,
389        }
390    }
391
392    /// Get a pinned mutable reference to the inner inner
393    pub(crate) fn get_pin_mut(self: Pin<&mut Self>) -> Pin<&mut S> {
394        self.project().inner
395    }
396}
397
398impl<S, T, E> Stream for StreamErrorIntoIoError<S, E>
399where
400    S: Stream<Item = Result<T, E>>,
401{
402    type Item = Result<T, io::Error>;
403
404    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
405        let this = self.project();
406        match ready!(this.inner.poll_next(cx)) {
407            None => Poll::Ready(None),
408            Some(Ok(value)) => {
409                *this.read_some_data = true;
410                Poll::Ready(Some(Ok(value)))
411            }
412            Some(Err(err)) => {
413                *this.error = Some(err);
414                Poll::Ready(Some(Err(io::Error::from_raw_os_error(SENTINEL_ERROR_CODE))))
415            }
416        }
417    }
418}
419
420pub(crate) const SENTINEL_ERROR_CODE: i32 = -837459418;
421
422/// Level of compression data should be compressed with.
423#[non_exhaustive]
424#[derive(Default, Clone, Copy, Debug, Eq, PartialEq, Hash)]
425pub enum CompressionLevel {
426    /// Fastest quality of compression, usually produces bigger size.
427    Fastest,
428    /// Best quality of compression, usually produces the smallest size.
429    Best,
430    /// Default quality of compression defined by the selected compression
431    /// algorithm.
432    #[default]
433    Default,
434    /// Precise quality based on the underlying compression algorithms'
435    /// qualities.
436    ///
437    /// The interpretation of this depends on the algorithm chosen and the
438    /// specific implementation backing it.
439    ///
440    /// Qualities are implicitly clamped to the algorithm's maximum.
441    Precise(u32),
442}
443
444use async_compression::Level as AsyncCompressionLevel;
445use compression_core::Level as CompressionCoreLevel;
446
447impl CompressionLevel {
448    #[allow(dead_code)]
449    pub(crate) fn into_async_compression(self) -> AsyncCompressionLevel {
450        match self {
451            Self::Fastest => AsyncCompressionLevel::Fastest,
452            Self::Best => AsyncCompressionLevel::Best,
453            Self::Default => AsyncCompressionLevel::Default,
454            Self::Precise(quality) => {
455                AsyncCompressionLevel::Precise(quality.try_into().unwrap_or(i32::MAX))
456            }
457        }
458    }
459}
460
461impl CompressionLevel {
462    #[allow(dead_code)]
463    pub(crate) fn into_compression_core(self) -> CompressionCoreLevel {
464        match self {
465            Self::Fastest => CompressionCoreLevel::Fastest,
466            Self::Best => CompressionCoreLevel::Best,
467            Self::Default => CompressionCoreLevel::Default,
468            Self::Precise(quality) => {
469                CompressionCoreLevel::Precise(quality.try_into().unwrap_or(i32::MAX))
470            }
471        }
472    }
473}