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>
change wqtvzultrxuxulkosmmwtmzppuqllvty
commit 7841c0b32abd46a2ff14788a7b16409d6924b282
author Alpha Chen <alpha@kejadlen.dev>
date
parent wqszzzxs
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");
+}