1use std::{convert::Infallible, future::ready};
2
3use http::{HeaderMap, header::ACCEPT_ENCODING};
4use tower::{ServiceExt, service_fn};
5use tower_http::compression::predicate::{NotForContentType, Predicate, SizeAbove};
6
7use crate::{Body, response::Response};
8
9#[derive(Clone, Debug)]
41pub struct Compression {
42 gzip: bool,
44 brotli: bool,
46 level: CompressionLevel,
48 min_size: u64,
50}
51
52impl Compression {
53 #[must_use]
57 pub fn new() -> Self {
58 const DEFAULT_MIN_SIZE: u64 = 32;
61
62 Self {
63 gzip: true,
64 brotli: true,
65 level: CompressionLevel::default(),
66 min_size: DEFAULT_MIN_SIZE,
67 }
68 }
69
70 #[must_use]
76 pub fn off() -> Self {
77 Self {
78 gzip: false,
79 brotli: false,
80 ..Self::new()
81 }
82 }
83
84 #[must_use]
86 pub fn gzip(mut self, enabled: bool) -> Self {
87 self.gzip = enabled;
88 self
89 }
90
91 #[must_use]
93 pub fn brotli(mut self, enabled: bool) -> Self {
94 self.brotli = enabled;
95 self
96 }
97
98 #[must_use]
100 pub fn level(mut self, level: CompressionLevel) -> Self {
101 self.level = level;
102 self
103 }
104
105 #[must_use]
111 pub fn min_size(mut self, bytes: u64) -> Self {
112 self.min_size = bytes;
113 self
114 }
115
116 pub(crate) async fn compress(
120 &self,
121 request_headers: &HeaderMap,
122 response: Response,
123 ) -> Response {
124 if !self.gzip && !self.brotli {
125 return response;
126 }
127
128 let mut request = http::Request::new(());
131 for value in request_headers.get_all(ACCEPT_ENCODING) {
132 request.headers_mut().append(ACCEPT_ENCODING, value.clone());
133 }
134
135 let predicate = SizeAbove::new(self.min_size)
138 .and(NotForContentType::GRPC)
139 .and(NotForContentType::IMAGES)
140 .and(NotForContentType::SSE);
141
142 let mut response = Some(response);
145 let inner = service_fn(move |_: http::Request<()>| {
146 let response = response.take().expect("one-shot service called once");
147 ready(Ok::<_, Infallible>(response))
148 });
149 let service = tower_http::compression::Compression::new(inner)
150 .gzip(self.gzip)
151 .br(self.brotli)
152 .quality(self.level.into_tower())
153 .compress_when(predicate);
154
155 match service.oneshot(request).await {
156 Ok(response) => response.map(Body::new),
157 Err(never) => match never {},
158 }
159 }
160}
161
162impl Default for Compression {
163 fn default() -> Self {
164 Self::new()
165 }
166}
167
168#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
171pub enum CompressionLevel {
172 Fastest,
174 #[default]
178 Balanced,
179 Best,
182 Precise(i32),
185}
186
187impl CompressionLevel {
188 fn into_tower(self) -> tower_http::CompressionLevel {
190 const BALANCED_QUALITY: i32 = 4;
192
193 match self {
194 Self::Fastest => tower_http::CompressionLevel::Fastest,
195 Self::Balanced => tower_http::CompressionLevel::Precise(BALANCED_QUALITY),
196 Self::Best => tower_http::CompressionLevel::Best,
197 Self::Precise(quality) => tower_http::CompressionLevel::Precise(quality),
198 }
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use std::{borrow::Cow, future::Future};
205
206 use http::{
207 HeaderValue,
208 header::{CONTENT_ENCODING, VARY},
209 };
210 use topcoat_core::context::Cx;
211
212 use super::*;
213 use crate::{
214 HeaderMap, Method, Path, RouteFn, RouteFuture, RouteHandlerFn, Router, request::Bytes,
215 response::IntoResponse, to_bytes,
216 };
217
218 fn block_on<F: Future>(future: F) -> F::Output {
221 tokio::runtime::Builder::new_current_thread()
222 .build()
223 .unwrap()
224 .block_on(future)
225 }
226
227 fn router_with(handler: RouteHandlerFn, compression: Compression) -> Router {
230 Router::builder()
231 .route(RouteFn::new(
232 Method::GET,
233 Cow::Borrowed(Path::new("/x")),
234 handler,
235 ))
236 .compression(compression)
237 .build()
238 }
239
240 fn send(router: &Router, accept_encoding: Option<&str>) -> (HeaderMap, Bytes) {
243 let mut request = http::Request::builder().uri("/x");
244 if let Some(value) = accept_encoding {
245 request = request.header(ACCEPT_ENCODING, value);
246 }
247 let response = block_on(router.handle(request.body(Body::empty()).unwrap()));
248 let (parts, body) = response.into_parts();
249 let bytes = block_on(to_bytes(body, usize::MAX)).unwrap();
250 (parts.headers, bytes)
251 }
252
253 fn long_body() -> String {
255 "route ".repeat(64)
256 }
257
258 fn long_route(cx: &Cx, _body: Body) -> RouteFuture<'_> {
259 Box::pin(async move { long_body().into_response(cx) })
260 }
261
262 fn short_route(cx: &Cx, _body: Body) -> RouteFuture<'_> {
264 Box::pin(async move { "route".into_response(cx) })
265 }
266
267 fn encoded_route(cx: &Cx, _body: Body) -> RouteFuture<'_> {
269 Box::pin(async move {
270 let mut response = long_body().into_response(cx)?;
271 response
272 .headers_mut()
273 .insert(CONTENT_ENCODING, HeaderValue::from_static("br"));
274 Ok(response)
275 })
276 }
277
278 #[test]
281 fn compresses_with_the_negotiated_algorithm() {
282 let router = router_with(long_route, Compression::new());
283
284 let (headers, body) = send(&router, Some("gzip"));
285 assert_eq!(headers.get(CONTENT_ENCODING).unwrap(), "gzip");
286 assert_eq!(headers.get(VARY).unwrap(), "accept-encoding");
287 assert!(!body.is_empty());
288 assert!(body.len() < long_body().len());
289
290 let (headers, _) = send(&router, Some("br"));
291 assert_eq!(headers.get(CONTENT_ENCODING).unwrap(), "br");
292 }
293
294 #[test]
295 fn passes_through_without_accept_encoding() {
296 let router = router_with(long_route, Compression::new());
297 let (headers, body) = send(&router, None);
298 assert!(!headers.contains_key(CONTENT_ENCODING));
299 assert_eq!(body, long_body());
300 }
301
302 #[test]
303 fn off_disables_compression() {
304 let router = router_with(long_route, Compression::off());
305 let (headers, body) = send(&router, Some("gzip, br"));
306 assert!(!headers.contains_key(CONTENT_ENCODING));
307 assert_eq!(body, long_body());
308 }
309
310 #[test]
311 fn disabled_algorithms_are_not_offered() {
312 let router = router_with(long_route, Compression::new().gzip(false));
313
314 let (headers, body) = send(&router, Some("gzip"));
316 assert!(!headers.contains_key(CONTENT_ENCODING));
317 assert_eq!(body, long_body());
318
319 let (headers, _) = send(&router, Some("gzip, br"));
321 assert_eq!(headers.get(CONTENT_ENCODING).unwrap(), "br");
322 }
323
324 #[test]
325 fn small_bodies_are_not_compressed() {
326 let router = router_with(short_route, Compression::new());
327 let (headers, body) = send(&router, Some("gzip"));
328 assert!(!headers.contains_key(CONTENT_ENCODING));
329 assert_eq!(&body[..], b"route");
330 }
331
332 #[test]
333 fn min_size_lowers_the_compression_threshold() {
334 let router = router_with(short_route, Compression::new().min_size(0));
335 let (headers, _) = send(&router, Some("gzip"));
336 assert_eq!(headers.get(CONTENT_ENCODING).unwrap(), "gzip");
337 }
338
339 #[test]
340 fn already_encoded_responses_pass_through() {
341 let router = router_with(encoded_route, Compression::new());
342 let (headers, body) = send(&router, Some("gzip"));
343 assert_eq!(headers.get(CONTENT_ENCODING).unwrap(), "br");
345 assert_eq!(body, long_body());
346 }
347}