perf(http): exclude file downloads from the global compression layer
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
This commit is contained in:
+23
-1
@@ -571,6 +571,26 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
use tower_http::compression::CompressionLayer;
|
use tower_http::compression::CompressionLayer;
|
||||||
use tower_http::compression::predicate::{NotForContentType, Predicate, SizeAbove};
|
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<B>(&self, response: &axum::http::Response<B>) -> bool
|
||||||
|
where
|
||||||
|
B: http_body::Body,
|
||||||
|
{
|
||||||
|
!response
|
||||||
|
.headers()
|
||||||
|
.contains_key(axum::http::header::CONTENT_DISPOSITION)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let predicate = SizeAbove::new(256)
|
let predicate = SizeAbove::new(256)
|
||||||
.and(NotForContentType::GRPC)
|
.and(NotForContentType::GRPC)
|
||||||
.and(NotForContentType::SSE)
|
.and(NotForContentType::SSE)
|
||||||
@@ -616,7 +636,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
// ── PDF: streams are usually already deflated; often large ──
|
// ── PDF: streams are usually already deflated; often large ──
|
||||||
.and(NotForContentType::const_new("application/pdf"))
|
.and(NotForContentType::const_new("application/pdf"))
|
||||||
// ── opaque binary we couldn't identify ──
|
// ── 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));
|
app = app.layer(CompressionLayer::new().compress_when(predicate));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user