Now let's take a silly one
1use http::Response;
2use http::header::CONTENT_TYPE;
3use http_body::Body;
4use tower_http::compression::CompressionLayer;
5use tower_http::compression::predicate::{And, Predicate, SizeAbove};
6
7const MIN_COMPRESS_BYTES: u64 = 256;
8
9#[derive(Clone, Copy)]
10pub(crate) struct CompressibleResponse;
11
12impl Predicate for CompressibleResponse {
13 fn should_compress<B>(&self, response: &Response<B>) -> bool
14 where
15 B: Body,
16 {
17 response
18 .headers()
19 .get(CONTENT_TYPE)
20 .and_then(|value| value.to_str().ok())
21 .map(|value| {
22 value
23 .split(';')
24 .next()
25 .unwrap_or(value)
26 .trim()
27 .to_ascii_lowercase()
28 })
29 .is_some_and(|base| {
30 matches!(
31 base.as_str(),
32 "application/json" | "application/x-git-upload-pack-advertisement"
33 )
34 })
35 }
36}
37
38pub(crate) fn layer() -> CompressionLayer<And<SizeAbove, CompressibleResponse>> {
39 CompressionLayer::new()
40 .compress_when(SizeAbove::new(MIN_COMPRESS_BYTES).and(CompressibleResponse))
41}
42
43#[cfg(test)]
44mod tests {
45 use axum::Router;
46 use axum::response::IntoResponse;
47 use axum::routing::get;
48 use http::StatusCode;
49 use http::header::{ACCEPT_ENCODING, CONTENT_ENCODING, CONTENT_RANGE, CONTENT_TYPE, RANGE};
50 use tower::ServiceExt;
51
52 use super::layer;
53
54 async fn json_body() -> impl IntoResponse {
55 ([(CONTENT_TYPE, "application/json")], "x".repeat(4096))
56 }
57
58 async fn advertisement() -> impl IntoResponse {
59 (
60 [(CONTENT_TYPE, "application/x-git-upload-pack-advertisement")],
61 "x".repeat(4096),
62 )
63 }
64
65 async fn pack_result() -> impl IntoResponse {
66 (
67 [(CONTENT_TYPE, "application/x-git-upload-pack-result")],
68 "x".repeat(4096),
69 )
70 }
71
72 async fn targz_archive() -> impl IntoResponse {
73 ([(CONTENT_TYPE, "application/gzip")], "x".repeat(4096))
74 }
75
76 async fn zip_archive() -> impl IntoResponse {
77 ([(CONTENT_TYPE, "application/zip")], "x".repeat(4096))
78 }
79
80 async fn small_json() -> impl IntoResponse {
81 ([(CONTENT_TYPE, "application/json")], "{}")
82 }
83
84 async fn cased_json() -> impl IntoResponse {
85 (
86 [(CONTENT_TYPE, "Application/JSON; charset=utf-8")],
87 "x".repeat(4096),
88 )
89 }
90
91 async fn partial_archive() -> impl IntoResponse {
92 (
93 StatusCode::PARTIAL_CONTENT,
94 [
95 (CONTENT_TYPE, "application/gzip"),
96 (CONTENT_RANGE, "bytes 0-3/4096"),
97 ],
98 "xxxx",
99 )
100 }
101
102 fn app() -> Router {
103 Router::new()
104 .route("/json", get(json_body))
105 .route("/adv", get(advertisement))
106 .route("/pack", get(pack_result))
107 .route("/targz", get(targz_archive))
108 .route("/zip", get(zip_archive))
109 .route("/small", get(small_json))
110 .route("/cased", get(cased_json))
111 .route("/partial", get(partial_archive))
112 .layer(layer())
113 }
114
115 async fn content_encoding(path: &str, accept: Option<&str>) -> Option<String> {
116 let mut builder = http::Request::builder().method("GET").uri(path);
117 if let Some(value) = accept {
118 builder = builder.header(ACCEPT_ENCODING, value);
119 }
120 let request = builder.body(axum::body::Body::empty()).unwrap();
121 app()
122 .oneshot(request)
123 .await
124 .unwrap()
125 .headers()
126 .get(CONTENT_ENCODING)
127 .map(|value| value.to_str().unwrap().to_string())
128 }
129
130 #[tokio::test]
131 async fn json_and_advertisement_compress_but_pack_bytes_pass_through() {
132 let negotiated = ["zstd", "br", "gzip"];
133 let json = content_encoding("/json", Some("zstd, br, gzip")).await;
134 assert!(
135 json.as_deref().is_some_and(|enc| negotiated.contains(&enc)),
136 "json negotiates an encoding, got {json:?}"
137 );
138 let adv = content_encoding("/adv", Some("zstd, br, gzip")).await;
139 assert!(
140 adv.as_deref().is_some_and(|enc| negotiated.contains(&enc)),
141 "the ref advertisement negotiates an encoding, got {adv:?}"
142 );
143 assert_eq!(
144 content_encoding("/pack", Some("zstd, br, gzip")).await,
145 None,
146 "an already-compressed pack stream is never re-encoded"
147 );
148 }
149
150 #[tokio::test]
151 async fn already_compressed_archives_pass_through() {
152 assert_eq!(
153 content_encoding("/targz", Some("zstd, br, gzip")).await,
154 None,
155 "a gzip archive is never re-encoded"
156 );
157 assert_eq!(
158 content_encoding("/zip", Some("zstd, br, gzip")).await,
159 None,
160 "a zip archive is never re-encoded"
161 );
162 }
163
164 #[tokio::test]
165 async fn zstd_outranks_brotli_outranks_gzip_on_equal_quality() {
166 assert_eq!(
167 content_encoding("/json", Some("gzip, br, zstd"))
168 .await
169 .as_deref(),
170 Some("zstd"),
171 "zstd wins over brotli and gzip at equal q"
172 );
173 assert_eq!(
174 content_encoding("/json", Some("gzip, br")).await.as_deref(),
175 Some("br"),
176 "brotli wins over gzip at equal q"
177 );
178 assert_eq!(
179 content_encoding("/json", Some("gzip")).await.as_deref(),
180 Some("gzip"),
181 "gzip serves the client that offers only gzip"
182 );
183 }
184
185 #[tokio::test]
186 async fn nothing_compresses_without_accept_encoding_or_below_the_floor() {
187 assert_eq!(content_encoding("/json", None).await, None);
188 assert_eq!(
189 content_encoding("/small", Some("zstd, br, gzip")).await,
190 None,
191 "a body below the size floor is left alone"
192 );
193 }
194
195 #[tokio::test]
196 async fn a_mixed_case_content_type_still_compresses() {
197 let negotiated = ["zstd", "br", "gzip"];
198 let cased = content_encoding("/cased", Some("zstd, br, gzip")).await;
199 assert!(
200 cased
201 .as_deref()
202 .is_some_and(|enc| negotiated.contains(&enc)),
203 "content-type matching is case insensitive, got {cased:?}"
204 );
205 }
206
207 #[tokio::test]
208 async fn a_partial_archive_passes_through_with_its_range_intact() {
209 let mut builder = http::Request::builder().method("GET").uri("/partial");
210 builder = builder.header(ACCEPT_ENCODING, "zstd, br, gzip");
211 builder = builder.header(RANGE, "bytes=0-3");
212 let response = app()
213 .oneshot(builder.body(axum::body::Body::empty()).unwrap())
214 .await
215 .unwrap();
216 assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
217 assert_eq!(
218 response.headers().get(CONTENT_RANGE).unwrap(),
219 "bytes 0-3/4096",
220 "the range survives the compression layer untouched"
221 );
222 assert_eq!(
223 response.headers().get(CONTENT_ENCODING),
224 None,
225 "a partial archive is never re-encoded"
226 );
227 }
228}