Request Decompression & Response Compression
Key Points
UseRequestDecompression(.NET 7+) — automatically decompresses request bodies when the client sendsContent-Encoding: gzip|br|deflate. Off by default.UseResponseCompression— compresses response bodies based onAccept-Encoding. Brotli > Gzip for text; configure providers explicitly.- Don't compress already-compressed payloads — JPEGs, MP4s, PNGs gain nothing and waste CPU. Filter by
Content-Type(default does this for common image/video types). - TLS termination in proxy? — many reverse proxies (nginx, AGW, AFD) compress for you; double-compressing wastes CPU. Configure one or the other.
- Senior risk awareness: response compression with HTTPS once enabled the BREACH attack family. Don't compress responses that contain user-controlled secrets in the same payload.
Concepts (deep dive)
Request decompression
Clients posting large JSON / files compress the body to save bandwidth:
Without UseRequestDecompression, your handler reads the gzip bytes — model binding fails. With it, the body is transparently decompressed on read.
builder.Services.AddRequestDecompression(); // default providers: gzip, br, deflate
var app = builder.Build();
app.UseRequestDecompression();
Custom provider:
builder.Services.AddRequestDecompression(opts =>
{
opts.DecompressionProviders.Add("custom", new MyDecompressionProvider());
});
Limits to be aware of: - Max input length controlled via Kestrel's MaxRequestBodySize. - Decompression bomb risk — a small gzip can expand to gigabytes. Set body size limits AND decompressed limits.
Response compression
builder.Services.AddResponseCompression(opts =>
{
opts.EnableForHttps = true; // BREACH-aware; only enable knowing risk
opts.Providers.Add<BrotliCompressionProvider>();
opts.Providers.Add<GzipCompressionProvider>();
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(["application/json"]);
});
builder.Services.Configure<BrotliCompressionProviderOptions>(o => o.Level = CompressionLevel.Optimal);
builder.Services.Configure<GzipCompressionProviderOptions>(o => o.Level = CompressionLevel.Fastest);
var app = builder.Build();
app.UseResponseCompression();
Brotli vs Gzip
Text (10 KB JSON):
Gzip Fastest: 2.4 KB 50 µs
Gzip Optimal: 1.9 KB 300 µs
Brotli Fastest: 1.7 KB 100 µs
Brotli Optimal: 1.4 KB 1 ms
Brotli compresses text better than gzip; both are well-supported in browsers. Default to Brotli + Gzip fallback; pick CompressionLevel.Fastest unless throughput justifies the CPU.
When NOT to compress
- Already-compressed: JPEG, PNG, GIF, MP4, MP3, ZIP, GZ, BR — compressing wastes CPU, sometimes grows.
- Real-time / latency-sensitive: low-RTT internal services may not benefit from the CPU cost.
- Below 1-2 KB: overhead exceeds savings.
- HTTPS + user-content reflection: BREACH attack risk (see security section).
ResponseCompressionDefaults.MimeTypes excludes media types by default; verify your app doesn't add them back.
BREACH attack briefing
If a TLS response contains: 1. A secret (CSRF token, session ID). 2. Attacker-controlled content reflected in the response. 3. Compression enabled.
The attacker can guess the secret one character at a time by observing compressed length differences. Mitigations: - Don't include secrets in compressed responses, or - Add randomness to responses (XSRF token randomized per request), or - Disable response compression for endpoints that include secrets.
Streaming and compression
Compression buffers internally. With chunked transfer or streaming, responses are compressed on the fly. Be aware: - Content-Length is not known up front; chunked encoding used. - Compression buffer sizes affect TTFB.
For Server-Sent Events / streaming JSON / huge file downloads, often skip compression to keep flush latency low.
Reverse proxy doing compression
If nginx / AGW / Cloudfront already compresses, configuring UseResponseCompression in the app double-compresses or wastes CPU — only one layer should compress. Common pattern:
Internal service → ALB (no compression) → app (compresses) → client
↑ direct
or
Internal service → AFD (compresses) → app (no compression) → client
Pick one. The proxy is usually faster (native code) but app-level compression is portable across deploys.
Custom compression provider
public class ZstdCompressionProvider : ICompressionProvider
{
public string EncodingName => "zstd";
public bool SupportsFlush => true;
public Stream CreateStream(Stream outputStream) => new ZstdNet.CompressionStream(outputStream);
}
Useful if a client asserts a non-standard encoding (private intranet pairs).
Code: correct vs wrong
❌ Wrong: enabling compression for everything
builder.Services.AddResponseCompression(); // defaults are sane but...
opts.EnableForHttps = true;
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes
.Concat(["image/jpeg", "video/mp4"]); // wastes CPU
✅ Correct: text + JSON, default exclusions
builder.Services.AddResponseCompression(opts =>
{
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(["application/json"]);
opts.EnableForHttps = true;
});
❌ Wrong: forgetting decompression bomb limits
builder.WebHost.ConfigureKestrel(opts => opts.Limits.MaxRequestBodySize = 100 * 1024 * 1024); // 100 MB
// + UseRequestDecompression — a 10 MB gzip can expand to 10 GB
✅ Correct: bound decompressed size
app.Use(async (ctx, next) =>
{
ctx.Request.EnableBuffering(bufferThreshold: 64 * 1024, bufferLimit: 100 * 1024 * 1024);
await next();
});
Or read the request body manually with a length-limited stream.
Design patterns for this topic
Pattern 1 — Brotli first, gzip fallback
builder.Services.AddResponseCompression(opts =>
{
opts.Providers.Add<BrotliCompressionProvider>();
opts.Providers.Add<GzipCompressionProvider>();
});
Order matters: first matching Accept-Encoding wins.
Pattern 2 — Skip compression on streaming endpoints
app.MapGet("/stream", async (HttpContext ctx) =>
{
ctx.Features.Get<IHttpResponseCompressionFeature>()!.DisableCompression();
// stream response
});
Pattern 3 — Validate content-length before decompression
builder.Services.AddRequestDecompression(opts => { /* defaults */ });
app.Use(async (ctx, next) =>
{
if (ctx.Request.ContentLength is > 50 * 1024 * 1024) // 50 MB compressed cap
{
ctx.Response.StatusCode = 413; return;
}
await next();
});
Pattern 4 — Disable for sensitive endpoints
For endpoints that return user-context-mixed-with-secrets, opt out:
[ResponseCompressionExempt] // your own attribute checked in middleware
public IActionResult GetSensitive() => Ok(...);
Pros & cons / trade-offs
| Choice | Pros | Cons |
|---|---|---|
| Compress text/JSON | Big bandwidth savings | Small CPU cost |
| Compress everything | Simple config | CPU waste on media |
| Brotli optimal | Smallest output | High CPU per request |
| Brotli fastest | Good ratio, low CPU | Slightly bigger than optimal |
| Proxy-level only | Native perf, central | Must coordinate with deploys |
| App-level only | Portable | CPU on app servers |
When to use / when to avoid
- Use response compression for text, JSON, HTML, CSS, JS, XML.
- Use request decompression for APIs accepting large payloads from compression-aware clients.
- Avoid for media types, streaming, latency-critical endpoints, BREACH-vulnerable secrets.
- Avoid doubling proxy + app compression.
Interview Q&A
Q1. Why is EnableForHttps = false the default? A. BREACH attack: an attacker who can inject content into a TLS-compressed response and observe its length can extract secrets character-by-character. Microsoft made you opt-in so you confirm you've thought about it.
Q2. Brotli vs Gzip — when each? A. Browsers support both. Brotli compresses better, slightly slower. Use Brotli + Gzip fallback. CompressionLevel.Fastest covers most cases; Optimal only if bandwidth >> CPU.
Q3. What's a decompression bomb? A. A small compressed payload that expands to a huge size — designed to exhaust memory/CPU on the server. Mitigate by bounding both MaxRequestBodySize (compressed) and decompressed read limits.
Q4. When skip compression? A. Already-compressed media (JPEG, MP4), tiny payloads (<1 KB), streaming endpoints (latency), and BREACH-sensitive responses with reflected user input + secrets.
Q5. How does the middleware decide what to compress? A. Match Accept-Encoding against registered providers; check Content-Type against allowed MIME list; verify response is large enough; skip if response stream is already encoded.
Q6. What does EnableForHttps = true change? A. Allows compression on HTTPS responses; default is HTTP only. Required behind HTTPS-terminated reverse proxies that re-encrypt to upstream.
Q7. Should the reverse proxy or the app compress? A. Either, not both. Proxy compression is faster (native code, central) but couples deploys. App compression is portable. Pick one and disable the other.
Q8. Can request decompression interact with rate limiting / size validation? A. Yes — order matters. Run size checks against the compressed body first; the request body limit middleware sees the wire bytes. Decompression then expands but you can apply additional length limits inside the handler.
Gotchas / common mistakes
MaxRequestBodySizebounding compressed size only — decompressed payload can be 10×+ larger.- Compressing media (JPEG/MP4) — wasted CPU, sometimes larger output.
- Both proxy and app compressing — recompresses; sometimes results in worse ratios.
- BREACH-vulnerable responses — secrets + reflected user input + compression on HTTPS.
- Streaming responses with compression — buffers; first-byte latency.
- Custom MIME types not in default whitelist — silent no-compress.
EnableForHttps = truewithout thinking through BREACH.- Forgetting
Vary: Accept-Encoding(added automatically) — caches must vary.