perf(http): single smart compression layer — stop compressing media on /api
The /api router added its own predicate-less CompressionLayer (routes.rs), nested inside the global predicate-aware one in main.rs. As the inner layer it compressed responses first, so the global predicate that skips already- compressed media was bypassed for every /api response: video/audio/image/zip downloads got Brotli-compressed (CPU + first-byte latency for ~0 bytes saved) and lost their Content-Length (forced to chunked -> no client progress bar). - Remove the redundant /api CompressionLayer; /api now flows through the single global layer in main.rs. - Make that predicate smarter: compress by default so nothing shrinkable is missed, and skip ONLY already-compressed types. It no longer blanket-excludes image/*, so image/svg+xml (text, ~70% shrink) now compresses; raster formats are listed individually. Added the previously-missed already-compressed types: Office (docx/xlsx/pptx), ODF, epub, jar, apk, 7z/rar/bzip2/zstd/xz, woff/woff2 fonts, icons. Net: media downloads keep Content-Length and skip pointless compression, while text/JSON/JS/CSS/SVG/XML/ttf/otf/wasm still compress. fmt + clippy clean. https://claude.ai/code/session_01UtfkS3nZF1vrF5jNAps6wV
This commit is contained in:
@@ -9,7 +9,7 @@ use axum::{
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use tower_http::{compression::CompressionLayer, trace::TraceLayer};
|
||||
use tower_http::trace::TraceLayer;
|
||||
use utoipa::OpenApi;
|
||||
|
||||
/// Liveness probe — returns 200 if the process is running, no DB check.
|
||||
@@ -579,10 +579,10 @@ pub fn create_api_routes(app_state: &Arc<AppState>) -> Router<Arc<AppState>> {
|
||||
.with_state(app_state.clone());
|
||||
router = router.nest("/users", users_router);
|
||||
|
||||
// Transparent compression (gzip + brotli) for all API responses.
|
||||
// tower-http negotiates via Accept-Encoding and skips already-compressed
|
||||
// content types automatically. No manual compression in handlers.
|
||||
router
|
||||
.layer(CompressionLayer::new().br(true).gzip(true))
|
||||
.layer(TraceLayer::new_for_http())
|
||||
// Compression is applied once, globally, in `main.rs` with a content-type
|
||||
// aware predicate that skips already-compressed media. Re-applying it here
|
||||
// would double-wrap `/api`: this inner layer (no predicate) would compress
|
||||
// media downloads, burning CPU for ~0 gain and stripping `Content-Length`.
|
||||
// So this router only adds tracing; compression is the global layer's job.
|
||||
router.layer(TraceLayer::new_for_http())
|
||||
}
|
||||
|
||||
+53
-8
@@ -541,25 +541,70 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
app = app.layer(DefaultBodyLimit::max(BODY_LIMIT));
|
||||
|
||||
// ── HTTP compression (gzip + Brotli) ─────────────────────────────────
|
||||
// Negotiates the best encoding via Accept-Encoding. Skips responses
|
||||
// that are already compressed or wouldn't benefit (images, video, etc.).
|
||||
// Compatible with a future reverse proxy — if the proxy sees
|
||||
// `Content-Encoding` it will pass the response through untouched.
|
||||
// Negotiates the best encoding via Accept-Encoding. Policy: compress
|
||||
// everything by default so no shrinkable response is ever missed (text,
|
||||
// JSON, JS/CSS, XML, SVG, fonts ttf/otf, WASM…), and skip ONLY content
|
||||
// that is already compressed — where a second pass burns CPU and adds
|
||||
// latency for ~0 bytes saved.
|
||||
//
|
||||
// We deliberately do NOT blanket-exclude `image/*`: `image/svg+xml` is
|
||||
// plain text and compresses ~70%, so the genuinely-compressed raster
|
||||
// formats are listed individually instead, leaving SVG compressible.
|
||||
//
|
||||
// This is the single, global compression layer (the `/api` router used to
|
||||
// add its own predicate-less one, which silently compressed media). It is
|
||||
// reverse-proxy friendly: a proxy that sees `Content-Encoding` passes
|
||||
// the response through untouched.
|
||||
{
|
||||
use tower_http::compression::CompressionLayer;
|
||||
use tower_http::compression::predicate::{NotForContentType, Predicate, SizeAbove};
|
||||
|
||||
let predicate = SizeAbove::new(256)
|
||||
.and(NotForContentType::GRPC)
|
||||
.and(NotForContentType::IMAGES)
|
||||
.and(NotForContentType::SSE)
|
||||
.and(NotForContentType::const_new("application/octet-stream"))
|
||||
// ── already-compressed raster images (SVG intentionally absent) ──
|
||||
.and(NotForContentType::const_new("image/jpeg"))
|
||||
.and(NotForContentType::const_new("image/png"))
|
||||
.and(NotForContentType::const_new("image/gif"))
|
||||
.and(NotForContentType::const_new("image/webp"))
|
||||
.and(NotForContentType::const_new("image/avif"))
|
||||
.and(NotForContentType::const_new("image/heic"))
|
||||
.and(NotForContentType::const_new("image/heif"))
|
||||
.and(NotForContentType::const_new("image/jp2"))
|
||||
.and(NotForContentType::const_new("image/x-icon"))
|
||||
.and(NotForContentType::const_new("image/vnd.microsoft.icon"))
|
||||
// ── audio / video families (already compressed) ──
|
||||
.and(NotForContentType::const_new("video/"))
|
||||
.and(NotForContentType::const_new("audio/"))
|
||||
// ── already-compressed web fonts; ttf/otf left compressible ──
|
||||
.and(NotForContentType::const_new("font/woff"))
|
||||
.and(NotForContentType::const_new("application/font-woff"))
|
||||
// ── archives & compressed containers ──
|
||||
.and(NotForContentType::const_new("application/zip"))
|
||||
.and(NotForContentType::const_new("application/gzip"))
|
||||
.and(NotForContentType::const_new("application/x-gzip"))
|
||||
.and(NotForContentType::const_new("application/x-tar"))
|
||||
.and(NotForContentType::const_new("application/x-7z-compressed"))
|
||||
.and(NotForContentType::const_new("application/x-rar-compressed"))
|
||||
.and(NotForContentType::const_new("application/x-bzip2"))
|
||||
.and(NotForContentType::const_new("application/zstd"))
|
||||
.and(NotForContentType::const_new("application/x-xz"))
|
||||
// ── zip-based document / app bundles (docx/xlsx/pptx, odf, epub…) ──
|
||||
.and(NotForContentType::const_new(
|
||||
"application/vnd.openxmlformats-officedocument",
|
||||
))
|
||||
.and(NotForContentType::const_new(
|
||||
"application/vnd.oasis.opendocument",
|
||||
))
|
||||
.and(NotForContentType::const_new("application/epub+zip"))
|
||||
.and(NotForContentType::const_new("application/java-archive"))
|
||||
.and(NotForContentType::const_new(
|
||||
"application/vnd.android.package-archive",
|
||||
))
|
||||
// ── PDF: streams are usually already deflated; often large ──
|
||||
.and(NotForContentType::const_new("application/pdf"))
|
||||
.and(NotForContentType::const_new("video/"))
|
||||
.and(NotForContentType::const_new("audio/"));
|
||||
// ── opaque binary we couldn't identify ──
|
||||
.and(NotForContentType::const_new("application/octet-stream"));
|
||||
|
||||
app = app.layer(CompressionLayer::new().compress_when(predicate));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user