routes: gzip responses
Pages are HTML with a row per listed message, which compresses about
twenty to one: the 3,000-message list goes from 1.0 MB to 45 KB on the
wire. Compressing in the app means it doesn't hinge on the proxy in
front doing it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
diff --git a/Cargo.lock b/Cargo.lock
index 96cc2e7..af784db 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -76,6 +76,18 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "async-compression"
+version = "0.4.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1"
+dependencies = [
+ "compression-codecs",
+ "compression-core",
+ "pin-project-lite",
+ "tokio",
+]
+
[[package]]
name = "atomic-waker"
version = "1.1.2"
@@ -172,7 +184,7 @@ dependencies = [
"addr2line",
"cfg-if",
"libc",
- "miniz_oxide",
+ "miniz_oxide 0.8.9",
"object",
"rustc-demangle",
"windows-link",
@@ -323,6 +335,23 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "compression-codecs"
+version = "0.4.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7"
+dependencies = [
+ "compression-core",
+ "flate2",
+ "memchr",
+]
+
+[[package]]
+name = "compression-core"
+version = "0.4.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d"
+
[[package]]
name = "core-foundation"
version = "0.9.4"
@@ -364,6 +393,15 @@ dependencies = [
"libc",
]
+[[package]]
+name = "crc32fast"
+version = "1.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78"
+dependencies = [
+ "cfg-if",
+]
+
[[package]]
name = "defmt"
version = "1.1.1"
@@ -493,6 +531,17 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
+[[package]]
+name = "flate2"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide 0.9.1",
+ "zlib-rs",
+]
+
[[package]]
name = "fnv"
version = "1.0.7"
@@ -1282,6 +1331,16 @@ dependencies = [
"adler2",
]
+[[package]]
+name = "miniz_oxide"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
[[package]]
name = "mio"
version = "1.2.3"
@@ -1959,6 +2018,12 @@ dependencies = [
"libc",
]
+[[package]]
+name = "simd-adler32"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
+
[[package]]
name = "simd_cesu8"
version = "1.2.0"
@@ -2321,12 +2386,16 @@ version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08a05a66a4fdd61cbbe0a1d755ffe0ca6aba159dd4820936a0ff8a8278245b9c"
dependencies = [
+ "async-compression",
"bitflags 2.13.2",
"bytes",
+ "futures-core",
"http",
"http-body",
"percent-encoding",
"pin-project-lite",
+ "tokio",
+ "tokio-util",
"tower-layer",
"tower-service",
"tracing",
@@ -2826,6 +2895,12 @@ dependencies = [
"syn 3.0.6",
]
+[[package]]
+name = "zlib-rs"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112"
+
[[package]]
name = "zmij"
version = "1.0.23"
diff --git a/Cargo.toml b/Cargo.toml
index f3faa63..70335be 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -30,7 +30,7 @@ serde_json = "*"
thiserror = "*"
tokio = { version = "*", features = ["full"] }
tower = "*"
-tower-http = { version = "*", features = ["trace"] }
+tower-http = { version = "*", features = ["compression-gzip", "trace"] }
tracing = "*"
tracing-subscriber = { version = "*", features = ["env-filter"] }
diff --git a/src/routes.rs b/src/routes.rs
index ca087d6..8989c69 100644
--- a/src/routes.rs
+++ b/src/routes.rs
@@ -9,6 +9,7 @@ use axum::routing::{get, post};
use axum::{Form, Router};
use maud::Markup;
use serde::Deserialize;
+use tower_http::compression::CompressionLayer;
use tower_http::trace::TraceLayer;
use crate::Error;
@@ -50,6 +51,7 @@ pub fn router(state: AppState) -> Router {
.route("/assets/gloss.css", get(gloss_css))
.route("/assets/docket.css", get(docket_css))
.route("/assets/alpine.js", get(alpine_js))
+ .layer(CompressionLayer::new())
.layer(TraceLayer::new_for_http())
.with_state(state)
}
diff --git a/tests/api.rs b/tests/api.rs
index 7415fd5..dc52495 100644
--- a/tests/api.rs
+++ b/tests/api.rs
@@ -542,3 +542,18 @@ async fn trashing_confirms_through_a_dialog_and_marks_done() {
.await;
assert_eq!(res.status(), StatusCode::FORBIDDEN);
}
+
+#[tokio::test]
+async fn pages_gzip_for_browsers_that_ask() {
+ let addr = spawn().await;
+ let res = client()
+ .get(format!("http://{addr}/"))
+ .header("Remote-User", SAM)
+ .header("X-User-Slug", slug(SAM))
+ .header("Accept-Encoding", "gzip")
+ .send()
+ .await
+ .unwrap();
+ assert_eq!(res.status(), StatusCode::OK);
+ assert_eq!(res.headers()["content-encoding"], "gzip");
+}