rama_http/layer/compression/stream/
service.rs1#![expect(
2 clippy::allow_attributes,
3 reason = "macro-generated `#[allow]` attributes whose underlying lints fire only for some expansions"
4)]
5
6use super::CompressionLevel;
7use super::body::StreamCompressionBody;
8use crate::HeaderValue;
9use crate::StreamingBody;
10use crate::headers::encoding::{
11 AcceptEncoding, Encoding, maybe_preferred_encoding_with_wildcard,
12 parse_accept_encoding_headers, parse_accept_encoding_wildcard_quality,
13};
14use crate::layer::compression::predicate::{DefaultStreamPredicate, Predicate, PreferredEncoding};
15use crate::layer::remove_header::remove_payload_metadata_headers;
16use crate::{Request, Response, StatusCode, header};
17use rama_core::Service;
18use rama_core::extensions::ExtensionsRef;
19use rama_core::telemetry::tracing;
20use rama_http_headers::HeaderMapExt;
21use rama_http_headers::TransferEncoding;
22use rama_http_headers::specifier::{Quality, QualityValue};
23use rama_http_types::Method;
24use rama_utils::collections::smallvec::SmallVec;
25use rama_utils::macros::define_inner_service_accessors;
26use rama_utils::str::submatch_ignore_ascii_case;
27
28#[derive(Debug, Clone)]
35pub struct StreamCompression<S, P = DefaultStreamPredicate> {
36 pub(crate) inner: S,
37 pub(crate) accept: AcceptEncoding,
38 pub(crate) predicate: P,
39 pub(crate) quality: CompressionLevel,
40 pub(crate) enforce_not_acceptable: bool,
41}
42
43impl<S> StreamCompression<S, DefaultStreamPredicate> {
44 pub fn new(service: S) -> Self {
46 Self {
47 inner: service,
48 accept: AcceptEncoding::default(),
49 predicate: DefaultStreamPredicate::default(),
50 quality: CompressionLevel::default(),
51 enforce_not_acceptable: true,
52 }
53 }
54}
55
56impl<S, P> StreamCompression<S, P> {
57 define_inner_service_accessors!();
58
59 rama_utils::macros::generate_set_and_with! {
60 pub fn gzip(mut self, enable: bool) -> Self {
62 self.accept.set_gzip(enable);
63 self
64 }
65 }
66
67 rama_utils::macros::generate_set_and_with! {
68 pub fn deflate(mut self, enable: bool) -> Self {
70 self.accept.set_deflate(enable);
71 self
72 }
73 }
74
75 rama_utils::macros::generate_set_and_with! {
76 pub fn br(mut self, enable: bool) -> Self {
78 self.accept.set_br(enable);
79 self
80 }
81 }
82
83 rama_utils::macros::generate_set_and_with! {
84 pub fn zstd(mut self, enable: bool) -> Self {
86 self.accept.set_zstd(enable);
87 self
88 }
89 }
90
91 rama_utils::macros::generate_set_and_with! {
92 pub fn quality(mut self, quality: CompressionLevel) -> Self {
94 self.quality = quality;
95 self
96 }
97 }
98
99 rama_utils::macros::generate_set_and_with! {
100 pub fn enforce_not_acceptable(mut self, enable: bool) -> Self {
107 self.enforce_not_acceptable = enable;
108 self
109 }
110 }
111
112 #[must_use]
121 pub fn with_compress_predicate<C>(self, predicate: C) -> StreamCompression<S, C>
122 where
123 C: Predicate,
124 {
125 StreamCompression {
126 inner: self.inner,
127 accept: self.accept,
128 predicate,
129 quality: self.quality,
130 enforce_not_acceptable: self.enforce_not_acceptable,
131 }
132 }
133}
134
135impl<ReqBody, ResBody, S, P> Service<Request<ReqBody>> for StreamCompression<S, P>
136where
137 S: Service<Request<ReqBody>, Output = Response<ResBody>>,
138 ResBody: StreamingBody<Data: Send + 'static, Error: Send + 'static> + Send + 'static,
139 P: Predicate + Send + Sync + 'static,
140 ReqBody: Send + 'static,
141{
142 type Output = Response<StreamCompressionBody<ResBody>>;
143 type Error = S::Error;
144
145 #[allow(unreachable_code, unused_mut, unused_variables, unreachable_patterns)]
146 async fn serve(&self, req: Request<ReqBody>) -> Result<Self::Output, Self::Error> {
147 let accepted_encodings: SmallVec<[QualityValue<Encoding>; 4]> =
148 parse_accept_encoding_headers(req.headers(), self.accept).collect();
149 let wildcard_quality = parse_accept_encoding_wildcard_quality(req.headers());
150 let req_method = req.method().clone();
151
152 let mut res = self.inner.serve(req).await?;
153
154 let body_allowed = !matches!(req_method, Method::HEAD | Method::CONNECT)
158 && !matches!(res.status().as_u16(), 100..=199 | 204 | 205 | 304);
159
160 let should_compress = body_allowed
161 && !res.headers().contains_key(header::CONTENT_ENCODING)
163 && !res.headers().contains_key(header::CONTENT_RANGE)
165 && self.predicate.should_compress(&mut res);
166
167 let negotiated = negotiate_response_encoding(
168 &accepted_encodings,
169 wildcard_quality,
170 self.accept,
171 res.extensions().get_ref::<PreferredEncoding>().copied(),
172 );
173
174 let encoding = match negotiated {
178 Some(encoding) => encoding,
179 None if self.enforce_not_acceptable && body_allowed => {
180 let (mut parts, body) = res.into_parts();
181 parts.status = StatusCode::NOT_ACCEPTABLE;
182 ensure_vary_accept_encoding(&mut parts.headers);
183 return Ok(Response::from_parts(
184 parts,
185 StreamCompressionBody::identity(body),
186 ));
187 }
188 None => Encoding::Identity,
189 };
190
191 let (mut parts, body) = res.into_parts();
192
193 if should_compress {
194 ensure_vary_accept_encoding(&mut parts.headers);
195 }
196
197 let always_flush = parts
198 .headers
199 .get("x-accel-buffering")
200 .and_then(|v| v.to_str().ok())
201 .is_some_and(|v| v.eq_ignore_ascii_case("no"))
202 || is_streaming_content_type(&parts.headers)
203 || is_chunked_encoding(&parts.headers);
204
205 tracing::trace!(
206 "should_compress = {should_compress}; always_flush = {always_flush}; encoding = {encoding}"
207 );
208
209 let body = match (should_compress, encoding) {
210 (false, _) | (_, Encoding::Identity) => {
212 return Ok(Response::from_parts(
213 parts,
214 StreamCompressionBody::identity(body),
215 ));
216 }
217
218 (true, Encoding::Gzip) => StreamCompressionBody::gzip(body, self.quality, always_flush),
219 (true, Encoding::Deflate) => {
220 StreamCompressionBody::deflate(body, self.quality, always_flush)
221 }
222 (true, Encoding::Brotli) => {
223 StreamCompressionBody::brotli(body, self.quality, always_flush)
224 }
225 (true, Encoding::Zstd) => StreamCompressionBody::zstd(body, self.quality, always_flush),
226 };
227
228 remove_payload_metadata_headers(&mut parts.headers);
229
230 parts
231 .headers
232 .insert(header::CONTENT_ENCODING, HeaderValue::from(encoding));
233
234 let res = Response::from_parts(parts, body);
235 Ok(res)
236 }
237}
238
239fn negotiate_response_encoding(
242 accepted_encodings: &[QualityValue<Encoding>],
243 wildcard_quality: Option<Quality>,
244 supported: AcceptEncoding,
245 preferred: Option<PreferredEncoding>,
246) -> Option<Encoding> {
247 if let Some(preferred) = preferred.map(PreferredEncoding::as_encoding)
248 && accepted_encodings
249 .iter()
250 .any(|qval| qval.value == preferred && qval.quality.as_u16() > 0)
251 {
252 return Some(preferred);
253 }
254
255 maybe_preferred_encoding_with_wildcard(accepted_encodings, wildcard_quality, supported)
256}
257
258fn ensure_vary_accept_encoding(headers: &mut header::HeaderMap) {
260 if !headers.get_all(header::VARY).iter().any(|value| {
261 submatch_ignore_ascii_case(
262 value.as_bytes(),
263 header::ACCEPT_ENCODING.as_str().as_bytes(),
264 )
265 }) {
266 headers.append(header::VARY, header::ACCEPT_ENCODING.into());
267 }
268}
269
270fn is_streaming_content_type(headers: &header::HeaderMap) -> bool {
271 headers
272 .get(header::CONTENT_TYPE)
273 .and_then(|v| v.to_str().ok())
274 .is_some_and(|ct| ct.starts_with("text/event-stream") || ct.starts_with("application/grpc"))
275}
276
277fn is_chunked_encoding(headers: &header::HeaderMap) -> bool {
278 !headers.contains_key(header::CONTENT_LENGTH)
279 || headers
280 .typed_get::<TransferEncoding>()
281 .map(|te| te.is_chunked())
282 .unwrap_or_default()
283}