Skip to main content

rama_http/layer/decompression/
service.rs

1use super::{DecompressedFrom, DecompressionBody, body::BodyInner};
2use crate::headers::encoding::{AcceptEncoding, SupportedEncodings};
3use crate::layer::remove_header::remove_payload_metadata_headers;
4use crate::layer::util::compression::{CompressionLevel, WrapBody};
5use crate::{
6    Request, Response, StreamingBody,
7    header::{self, ACCEPT_ENCODING},
8};
9use rama_core::error::{BoxError, ErrorContext as _};
10use rama_core::{
11    Service,
12    matcher::service::{ServiceMatch, ServiceMatcher},
13};
14use rama_utils::macros::define_inner_service_accessors;
15use std::convert::Infallible;
16
17/// Decompresses response bodies of the underlying service.
18///
19/// This adds the `Accept-Encoding` header to requests and transparently decompresses response
20/// bodies based on the `Content-Encoding` header.
21///
22/// See the [module docs](crate::layer::decompression) for more details.
23#[derive(Debug, Clone)]
24pub struct Decompression<S, M = DefaultDecompressionMatcher> {
25    pub(crate) inner: S,
26    pub(crate) accept: AcceptEncoding,
27    pub(crate) insert_accept_encoding_header: bool,
28    pub(crate) tolerate_decode_errors: bool,
29    pub(crate) matcher: M,
30}
31
32impl<S> Decompression<S> {
33    /// Creates a new `Decompression` wrapping the `service`.
34    pub fn new(service: S) -> Self {
35        Self {
36            inner: service,
37            accept: AcceptEncoding::default(),
38            insert_accept_encoding_header: true,
39            tolerate_decode_errors: false,
40            matcher: DefaultDecompressionMatcher,
41        }
42    }
43}
44
45impl<S, M> Decompression<S, M> {
46    define_inner_service_accessors!();
47
48    rama_utils::macros::generate_set_and_with! {
49        /// Sets whether the layer inserts `Accept-Encoding` into requests when it is absent.
50        ///
51        /// When disabled, the request header is forwarded as-is and the layer only advertises
52        /// supported encodings if the request already contains an `Accept-Encoding` header.
53        pub fn insert_accept_encoding_header(mut self, insert: bool) -> Self {
54            self.insert_accept_encoding_header = insert;
55            self
56        }
57    }
58
59    /// Replaces the request/response decompression matcher.
60    ///
61    /// The matcher runs at request time and may select a second matcher to evaluate the response
62    /// after the inner service returns. If no response matcher is selected or if the selected
63    /// response matcher does not match, the response is left compressed even when Rama supports
64    /// decompressing it.
65    pub fn with_matcher<T>(self, matcher: T) -> Decompression<S, T> {
66        Decompression {
67            inner: self.inner,
68            accept: self.accept,
69            insert_accept_encoding_header: self.insert_accept_encoding_header,
70            tolerate_decode_errors: self.tolerate_decode_errors,
71            matcher,
72        }
73    }
74
75    rama_utils::macros::generate_set_and_with! {
76        /// End a response body cleanly on a mid-stream decode error (after some
77        /// data decoded) instead of surfacing the error.
78        ///
79        /// For relays that decode → modify → re-encode a response (e.g. a MITM
80        /// HTML rewriter): a truncated upstream body yields a short-but-well-formed
81        /// client stream rather than an aborted one (RST_STREAM). Off by default;
82        /// genuine upstream transport errors still propagate.
83        pub fn tolerate_decode_errors(mut self, tolerate: bool) -> Self {
84            self.tolerate_decode_errors = tolerate;
85            self
86        }
87    }
88
89    rama_utils::macros::generate_set_and_with! {
90        /// Sets whether to request the gzip encoding.
91        pub fn gzip(mut self, enable: bool) -> Self {
92            self.accept.set_gzip(enable);
93            self
94        }
95    }
96
97    rama_utils::macros::generate_set_and_with! {
98        /// Sets whether to request the Deflate encoding.
99        pub fn deflate(mut self, enable: bool) -> Self {
100            self.accept.set_deflate(enable);
101            self
102        }
103    }
104
105    rama_utils::macros::generate_set_and_with! {
106        /// Sets whether to request the Brotli encoding.
107        pub fn br(mut self, enable: bool) -> Self {
108            self.accept.set_br(enable);
109            self
110        }
111    }
112
113    rama_utils::macros::generate_set_and_with! {
114        /// Sets whether to request the Zstd encoding.
115        pub fn zstd(mut self, enable: bool) -> Self {
116            self.accept.set_zstd(enable);
117            self
118        }
119    }
120}
121
122#[derive(Debug, Clone, Copy, Default)]
123/// Default request-time matcher for decompression.
124///
125/// It always enables a response-time decompression evaluation.
126pub struct DefaultDecompressionMatcher;
127
128#[derive(Debug, Clone, Copy, Default)]
129/// Default response-time matcher for decompression.
130///
131/// It always allows response decompression when Rama supports the advertised encoding.
132pub struct DefaultResponseDecompressionMatcher;
133
134impl<ReqBody> ServiceMatcher<Request<ReqBody>> for DefaultDecompressionMatcher
135where
136    ReqBody: Send + 'static,
137{
138    type Service = DefaultResponseDecompressionMatcher;
139    type Error = Infallible;
140    type ModifiedInput = Request<ReqBody>;
141
142    async fn match_service(
143        &self,
144        input: Request<ReqBody>,
145    ) -> Result<ServiceMatch<Self::ModifiedInput, Self::Service>, Self::Error> {
146        Ok(ServiceMatch {
147            input,
148            service: Some(DefaultResponseDecompressionMatcher),
149        })
150    }
151}
152
153impl<ResBody> ServiceMatcher<Response<ResBody>> for DefaultResponseDecompressionMatcher
154where
155    ResBody: Send + 'static,
156{
157    type Service = ();
158    type Error = Infallible;
159    type ModifiedInput = Response<ResBody>;
160
161    async fn match_service(
162        &self,
163        input: Response<ResBody>,
164    ) -> Result<ServiceMatch<Self::ModifiedInput, Self::Service>, Self::Error> {
165        Ok(ServiceMatch {
166            input,
167            service: Some(()),
168        })
169    }
170}
171
172impl<S, M, ReqBody, ResBody> Service<Request<ReqBody>> for Decompression<S, M>
173where
174    S: Service<Request<ReqBody>, Output = Response<ResBody>, Error: Into<BoxError>>,
175    M: ServiceMatcher<
176            Request<ReqBody>,
177            ModifiedInput = Request<ReqBody>,
178            Service: ServiceMatcher<
179                Response<ResBody>,
180                ModifiedInput = Response<ResBody>,
181                Service = (),
182                Error: Into<BoxError>,
183            >,
184            Error: Into<BoxError>,
185        >,
186    ReqBody: Send + 'static,
187    ResBody: StreamingBody<Data: Send + 'static, Error: Send + 'static> + Send + 'static,
188{
189    type Output = Response<DecompressionBody<ResBody>>;
190    type Error = BoxError;
191
192    async fn serve(&self, req: Request<ReqBody>) -> Result<Self::Output, Self::Error> {
193        let ServiceMatch {
194            input: mut req,
195            service: maybe_response_matcher,
196        } = self
197            .matcher
198            .match_service(req)
199            .await
200            .context("decompression matcher: request")?;
201
202        if self.insert_accept_encoding_header
203            && let header::Entry::Vacant(entry) = req.headers_mut().entry(ACCEPT_ENCODING)
204            && let Some(accept) = self.accept.maybe_to_header_value()
205        {
206            entry.insert(accept);
207        }
208
209        let res = self.inner.serve(req).await.context("inner::serve")?;
210
211        let ServiceMatch {
212            input: res,
213            service: should_decompress,
214        } = if let Some(response_matcher) = maybe_response_matcher {
215            response_matcher
216                .into_match_service(res)
217                .await
218                .context("decompression matcher: response")?
219        } else {
220            ServiceMatch {
221                input: res,
222                service: None,
223            }
224        };
225
226        let (mut parts, body) = res.into_parts();
227
228        let res = if should_decompress.is_some()
229            && let header::Entry::Occupied(entry) = parts.headers.entry(header::CONTENT_ENCODING)
230        {
231            let maybe_marker = match entry.get().as_bytes() {
232                b"gzip" if self.accept.gzip() => Some(DecompressedFrom::Gzip),
233                b"deflate" if self.accept.deflate() => Some(DecompressedFrom::Deflate),
234                b"br" if self.accept.br() => Some(DecompressedFrom::Brotli),
235                b"zstd" if self.accept.zstd() => Some(DecompressedFrom::Zstd),
236                _ => None,
237            };
238
239            let Some(marker) = maybe_marker else {
240                return Ok(Response::from_parts(
241                    parts,
242                    DecompressionBody::new(BodyInner::identity(body)),
243                ));
244            };
245
246            let body = match marker {
247                DecompressedFrom::Gzip => DecompressionBody::new(BodyInner::gzip(
248                    WrapBody::new(body, CompressionLevel::default())
249                        .with_tolerate_decode_errors(self.tolerate_decode_errors),
250                )),
251                DecompressedFrom::Deflate => DecompressionBody::new(BodyInner::deflate(
252                    WrapBody::new(body, CompressionLevel::default())
253                        .with_tolerate_decode_errors(self.tolerate_decode_errors),
254                )),
255                DecompressedFrom::Brotli => DecompressionBody::new(BodyInner::brotli(
256                    WrapBody::new(body, CompressionLevel::default())
257                        .with_tolerate_decode_errors(self.tolerate_decode_errors),
258                )),
259                DecompressedFrom::Zstd => DecompressionBody::new(BodyInner::zstd(
260                    WrapBody::new(body, CompressionLevel::default())
261                        .with_tolerate_decode_errors(self.tolerate_decode_errors),
262                )),
263            };
264
265            entry.remove();
266            remove_payload_metadata_headers(&mut parts.headers);
267            parts.extensions.insert(marker);
268
269            Response::from_parts(parts, body)
270        } else {
271            Response::from_parts(parts, DecompressionBody::new(BodyInner::identity(body)))
272        };
273
274        Ok(res)
275    }
276}