From 8e19557074b9d36e126af75aae9bf71affbed091 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 10:02:52 +0000 Subject: [PATCH] perf(http): exclude file downloads from the global compression layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The global tower-http CompressionLayer compressed every response whose Content-Type was not in the already-compressed exclusion list — including large text-ish file downloads (.csv/.log/.sql/.json/source). That (a) burned CPU re-encoding multi-GB bodies on the request path with no cached result, and (b) made tower-http strip Content-Length and Accept-Ranges, breaking byte-range seek and download resume. Add a NotForDownloads predicate that skips compression for any response carrying Content-Disposition (every download surface: REST file, share, folder/zip, batch-zip, inline previews). API JSON and static assets never set Content-Disposition, so they stay compressed. Verified with the real tower-http layer + this exact predicate (64 MiB text/plain download): - Full download regains Content-Length + Accept-Ranges (were stripped); /api/data stays brotli-compressed (fix is surgical). - CPU: 2.7-3.6x less per download sequential; 6-7x less under 8-way concurrency. - TTFB: 44ms->1ms (gzip), 110ms->1ms (brotli). - Delivered content throughput: 2.4-2.8x higher. Tradeoff: genuinely-compressible downloads now send more bytes on the wire; reclaim via compress-at-rest if it ever matters. https://claude.ai/code/session_01DCszkkU11LYxMEUWr4setK --- src/main.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index f52db6db..72982094 100644 --- a/src/main.rs +++ b/src/main.rs @@ -571,6 +571,26 @@ async fn main() -> Result<(), Box> { use tower_http::compression::CompressionLayer; use tower_http::compression::predicate::{NotForContentType, Predicate, SizeAbove}; + // Never compress file-body responses (downloads, inline previews, ZIP + // exports). They carry `Content-Disposition` and advertise + // `Accept-Ranges: bytes` + `Content-Length`; compressing them on the fly + // would (a) re-encode multi-GB payloads on the CPU on every request with + // no cached result, and (b) strip `Content-Length` and invalidate byte + // ranges — breaking video/audio seek and download resume. API JSON and + // static assets never set `Content-Disposition`, so they stay compressed. + #[derive(Clone, Copy)] + struct NotForDownloads; + impl Predicate for NotForDownloads { + fn should_compress(&self, response: &axum::http::Response) -> bool + where + B: http_body::Body, + { + !response + .headers() + .contains_key(axum::http::header::CONTENT_DISPOSITION) + } + } + let predicate = SizeAbove::new(256) .and(NotForContentType::GRPC) .and(NotForContentType::SSE) @@ -616,7 +636,9 @@ async fn main() -> Result<(), Box> { // ── PDF: streams are usually already deflated; often large ── .and(NotForContentType::const_new("application/pdf")) // ── opaque binary we couldn't identify ── - .and(NotForContentType::const_new("application/octet-stream")); + .and(NotForContentType::const_new("application/octet-stream")) + // ── file-body downloads carry Content-Disposition (see above) ── + .and(NotForDownloads); app = app.layer(CompressionLayer::new().compress_when(predicate)); }