.NET 11 HTTP request compression finally gives HttpClient a straightforward way to stream gzip, Brotli, or Zstandard request bodies. The new wrappers remove a familiar pile of custom stream code, but they do not make compression automatic. The server, gateway, and client still need an explicit agreement about the selected content coding.

The safe pattern is simple: enable request decompression on the API first, reject encodings outside the API contract, cap the decompressed body, and only then wrap selected client payloads. Verification must prove both sides of the contract—the bytes on the wire are compressed, and the endpoint receives the original model.

How .NET 11 HTTP request compression works

.NET 11 Preview 7 adds three HttpContent wrappers in System.Net.Http: GZipCompressedContent, BrotliCompressedContent, and ZstandardCompressedContent. Each wrapper preserves the inner content as the logical payload, sets the matching Content-Encoding header, removes a precomputed Content-Length, and compresses while the request is serialized. That streaming behavior matters for large JSON or batch payloads because the client does not need to build a second compressed byte array first.

Each type has a convenient constructor that accepts CompressionLevel and another that accepts the algorithm’s full options object. Start with the simple constructor unless measurements show a reason to tune quality, window size, or another algorithm-specific setting.

What did not change is equally important. HttpClient does not negotiate request compression. An Accept-Encoding response header describes response coding, not permission to compress a later request body. The client therefore needs configuration or API metadata that says a particular endpoint accepts gzip, br, or zstd.

Decide when request compression belongs

Compression pays for CPU to reduce transferred bytes. It is usually worth evaluating for repetitive text formats—large JSON documents, telemetry batches, bulk commands, or XML—when network transfer is a meaningful part of latency or cost. Tiny requests can become larger after framing, while JPEG, PNG, ZIP, and many document formats are already compressed and should normally pass through unchanged.

Use a policy instead of wrapping every request. A practical policy names the eligible endpoints, the minimum uncompressed size, the permitted algorithms, and the rollout fallback. It should also state whether intermediaries such as a CDN, reverse proxy, web application firewall, or API gateway preserve and accept Content-Encoding on requests. A server feature that works directly against Kestrel can still fail at the first proxy hop.

  • Prefer gzip when compatibility is the primary constraint.
  • Consider Brotli or Zstandard when both ends are controlled and measurements justify them.
  • Skip compression below a measured threshold rather than copying a universal byte count.
  • Never compress merely to conceal an endpoint that accepts unnecessarily large commands.

Configure ASP.NET Core before the client

ASP.NET Core request decompression middleware examines Content-Encoding and replaces HttpRequest.Body with a decompression stream when a registered provider matches. Decompression is lazy: it happens when model binding or endpoint code reads the body. In .NET 11, the default providers include gzip, Brotli, deflate, and Zstandard.

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRequestDecompression();

var app = builder.Build();

var supportedEncodings = new HashSet<string>(
    ["gzip", "br", "zstd"],
    StringComparer.OrdinalIgnoreCase);

app.Use(async (context, next) =>
{
    string raw = context.Request.Headers.ContentEncoding.ToString();
    string[] encodings = raw.Split(
        ',',
        StringSplitOptions.RemoveEmptyEntries |
        StringSplitOptions.TrimEntries);

    if (encodings.Length > 1 ||
        (encodings.Length == 1 && !supportedEncodings.Contains(encodings[0])))
    {
        context.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType;
        await context.Response.WriteAsync("Unsupported request content encoding.");
        return;
    }

    await next();
});

app.UseRequestDecompression();

app.MapPost("/imports", (ImportCommand command) =>
    Results.Accepted(value: new { command.BatchId, command.Items.Count }))
    .WithMetadata(new RequestSizeLimitAttribute(8 * 1024 * 1024));

app.Run();

public sealed record ImportCommand(
    Guid BatchId,
    IReadOnlyList<ImportItem> Items);

public sealed record ImportItem(string Key, decimal Value);

The first middleware turns an ambiguous downstream parsing failure into an explicit 415 Unsupported Media Type. It also rejects stacked encodings, which the decompression middleware does not process. Place it before UseRequestDecompression; place decompression before endpoints that bind the body.

The 8 MiB limit applies to bytes read from the decompressed stream, not just the smaller wire representation. This is the protection that matters against decompression bombs. Choose a limit from the endpoint’s real business envelope, keep normal authentication and authorization in place, and avoid DisableRequestSizeLimit on compressed upload routes.

Wrap HttpContent on the client

Once the deployed server path accepts a known coding, the client can wrap its normal content. The inner content retains the media type, while the outer wrapper owns the wire encoding. The following client uses Zstandard for one endpoint because that capability is part of its configuration, not because it guessed from a response.

If your application already centralizes authentication and API calls, keep that boundary separate from compression policy. Our guide to a reusable HttpClient service in ASP.NET Core shows one such client boundary; compression should remain an endpoint capability layered on top, not a global behavior.

using System.IO.Compression;
using System.Net.Http.Json;

public sealed class ImportClient(HttpClient httpClient)
{
    public async Task SubmitAsync(
        ImportCommand command,
        CancellationToken cancellationToken)
    {
        using HttpContent json = JsonContent.Create(command);
        using HttpContent compressed = new ZstandardCompressedContent(
            json,
            CompressionLevel.Fastest);

        using var request = new HttpRequestMessage(HttpMethod.Post, "imports")
        {
            Content = compressed
        };

        using HttpResponseMessage response = await httpClient.SendAsync(
            request,
            HttpCompletionOption.ResponseHeadersRead,
            cancellationToken);

        response.EnsureSuccessStatusCode();
    }
}

The important line is the wrapper, not a manual header assignment. Setting Content-Encoding without actually transforming the bytes produces a malformed request. Disposing the outer content also disposes the inner content, so keep both scoped to one send. Fastest is a reasonable first rollout choice for latency-sensitive calls; compare it with Optimal using representative payloads before changing the default.

Do not automatically retry a rejected POST with an uncompressed body unless the operation has a real idempotency contract. A timeout can leave the client uncertain about whether the server applied the first request. Prefer capability configuration that fails before sending, or combine any fallback with an idempotency key enforced by the API.

Verify the contract end to end

A green 202 is not enough. The test needs to show that the client sent the intended header, an intermediary did not strip it, ASP.NET Core decompressed the body, and model binding recovered the original values. Use a disposable test environment with the same proxy path as production when possible.

  • Send a compressible payload large enough to cross the application’s threshold.
  • Capture the request at the server edge and confirm a single Content-Encoding: zstd value.
  • Assert the endpoint receives the original batch ID and item count.
  • Repeat with an unsupported coding and expect the deliberate 415.
  • Send corrupted compressed bytes and verify the API returns a controlled client error without logging the body.
  • Send a highly compressible body whose expanded size exceeds the endpoint limit and verify it is rejected.

For an isolated client-side check, serialize a representative wrapper to bytes and decompress it with the matching stream. This catches accidental header-only implementations and confirms that the wrapper preserves the payload:

using System.IO.Compression;
using System.Text;

const string original = "{\"batchId\":\"5ed0cbb4-3a0f-4c7f-a3d8-b5bd27ea953e\"}";

using var inner = new StringContent(
    original,
    Encoding.UTF8,
    "application/json");
using var compressed = new ZstandardCompressedContent(inner);

byte[] wireBytes = await compressed.ReadAsByteArrayAsync();

await using var input = new MemoryStream(wireBytes);
await using var decoder = new ZstandardStream(
    input,
    CompressionMode.Decompress);
using var reader = new StreamReader(decoder, Encoding.UTF8);

string roundTrip = await reader.ReadToEndAsync();
if (roundTrip != original)
{
    throw new InvalidOperationException("Compressed request did not round-trip.");
}

if (!compressed.Headers.ContentEncoding.Contains("zstd"))
{
    throw new InvalidOperationException("Missing zstd Content-Encoding.");
}

This small check validates the new framework surface without pretending to validate the deployment. Keep the end-to-end test as the release gate because proxies, request limits, and middleware order only exist in the real path.

Handle production failure modes

Most request-compression incidents are contract failures rather than compression-library failures. An older server may not know zstd; a gateway may reject request coding; a client may send two encodings; or corrupted bytes may fail only when the endpoint begins reading the body. ASP.NET Core passes unsupported and multiply encoded requests onward when it cannot select a provider, so the explicit pre-check above avoids turning those cases into confusing JSON errors.

Invalid compressed data can raise InvalidDataException for gzip, deflate, or Zstandard, while Brotli can raise InvalidOperationException. Handle malformed-body failures through the application’s normal exception mapping and return a bounded 4xx response. Do not echo compressed bytes, decompressed payloads, credentials, or personal data into logs.

Zstandard has an extra interoperability constraint: RFC 9659 limits the decoder window for HTTP content coding to 8 MiB. The .NET 11 request wrapper enforces that requirement, but non-.NET clients and servers still need compatible implementations. Treat algorithm support as a versioned API capability rather than a global toggle.

Roll out without hiding regressions

Begin with one high-volume endpoint and one controlled client. Record uncompressed bytes, transmitted bytes, compression time, total request latency, CPU, 415 responses, decompression failures, and expanded-size rejections. These are operational measurements, not promises: the winning algorithm and threshold depend on payload shape and infrastructure.

Roll back through configuration by stopping new compressed sends while leaving server decompression enabled during the compatibility window. That order prevents older client deployments or queued jobs from failing during rollback. Remove an encoding from the server only after telemetry shows no remaining callers.

The new .NET 11 wrappers make the mechanics pleasantly small. The engineering work is still the contract around them: known endpoint capability, middleware order, decompressed-size enforcement, safe failure behavior, and a test that crosses the same hops as production. Put those pieces in place and .NET 11 HTTP request compression becomes a measured transport optimization instead of a hidden source of 400 and 415 responses.

References

Found this useful? Support more practical developer content.

Author

Practical .NET, Angular, Azure, Blazor, and AI engineering for real-world development.

Write A Comment

Ads Blocker Image Powered by Code Help Pro

Ads Blocker Detected!!!

We have detected that you are using extensions to block ads. Please support us by disabling these ads blocker.

Powered By
100% Free SEO Tools - Tool Kits PRO