Skip to content

feat(body): compress request bodies - #2311

Merged
glennawatson merged 3 commits into
mainfrom
feat/request-body-compression
Aug 18, 2026
Merged

feat(body): compress request bodies#2311
glennawatson merged 3 commits into
mainfrom
feat/request-body-compression

Conversation

@glennawatson

@glennawatson glennawatson commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

What kind of change does this PR introduce?

Feature.

What is the new behavior?

A method can send its request body under a content coding, per endpoint or per client.

  • [Body(Compression = ...)] sets the coding for one method. Refit compresses whatever the serializer produced and sets Content-Encoding to match, so the coding composes with every BodySerializationMethod - JSON, JSON Lines, URL-encoded forms and raw strings alike.

    [Post("/measurements")]
    Task Upload([Body(Compression = RequestCompression.GZip)] Measurement[] batch);
    
    [Post("/measurements")]
    Task UploadSmallest([Body(
        Compression = RequestCompression.Brotli,
        CompressionLevel = CompressionLevel.SmallestSize)] Measurement[] batch);
  • RefitSettings.RequestCompression sets a client-wide default. A [Body] parameter naming its own coding overrides it, and RequestCompression.None on the parameter opts one method out of a coding the settings turned on.

  • The coding is applied by the framework's own wrappers where they exist. .NET 11.0 uses GZipCompressedContent, BrotliCompressedContent and ZstandardCompressedContent; older targets compress through GZipStream or BrotliStream. Both request paths - the source-generated client and the reflection request builder - go through one entry point, so they cannot diverge.

  • RefitSettings.RequestCompressionOptions sets the compressor's own knobs - window size, strategy, a Zstandard dictionary - which a CompressionLevel cannot express. Options set for a coding replace the level for that coding; codings left unset still compress by level. The options types arrived with .NET 9.0 and Zstandard's with .NET 11.0, so this surface only exists from net9.0 onward.

    var settings = new RefitSettings
    {
        RequestCompression = RequestCompression.Brotli,
        RequestCompressionOptions = new()
        {
            Brotli = new() { Quality = 9, WindowLog2 = 22 },
            GZip = new() { CompressionLevel = 6 },
        },
    };
  • An unavailable coding throws rather than sending the body uncompressed. PlatformNotSupportedException is raised when the request is built, naming what the target framework can do:

    Coding Content-Encoding Available on
    RequestCompression.GZip gzip every target
    RequestCompression.Brotli br .NET 8.0 and later
    RequestCompression.Zstandard zstd .NET 11.0 and later

What is the current behavior?

Refit sends every request body uncompressed.

  • Compressing one required a DelegatingHandler that re-wraps content on the way out, which cannot tell one endpoint from another without inspecting the URI.

Closes #2307

What might this PR break?

None.

  • The default on both [Body] and RefitSettings leaves bodies uncompressed, so an existing client sends exactly what it sent before.
  • Generated clients now emit a CompressBodyContent call after the body assignment. It returns the content unchanged when no coding applies, which is what makes the settings-level default reach generated code.
  • The compressed length is unknown until the body has been written, so a compressed request is sent chunked with no Content-Length.

Checklist

  • I have read the Contribute guide
  • Tests have been added or updated (for bug fixes / features)
  • Docs have been added or updated (for bug fixes / features)
  • Changes target the main branch
  • PR title follows Conventional Commits

Additional information

There is no negotiation for a compressed request body, so this is opt-in and the README says to turn it on only against a server known to accept one.

The .NET 11.0 content wrappers are still preview API. RequestCompression is Refit's own enum, so the coding surface is insulated from them; RequestCompressionOptions does expose ZLibCompressionOptions, BrotliCompressionOptions and ZstandardCompressionOptions directly, which is deliberate - mirroring those knobs into Refit types would be a surface to maintain forever and would drift from the framework.

Hand-authored files worth reviewing: RequestContentCoding.cs (resolution and per-target dispatch), CompressedContent.cs (the pre-net11.0 wrapper), RequestCompressionOptions.cs, and Emitter.Inline.Content.cs (the emitted wrap). The PublicAPI.txt diffs are the analyzer's own baseline fixer output.

- Add `[Body(Compression = ...)]` so a method can send its body under a
  content coding. Refit compresses whatever the serializer produced and sets
  Content-Encoding, so the coding composes with every serialization method.
- Add RefitSettings.RequestCompression for a client-wide default. A [Body]
  parameter naming its own coding overrides it, and None opts one method out.
- Use the framework's GZipCompressedContent, BrotliCompressedContent and
  ZstandardCompressedContent on net11.0, and compress through GZipStream or
  BrotliStream below it.
- Throw PlatformNotSupportedException for a coding this framework cannot
  produce rather than quietly sending the body uncompressed. gzip is always
  available, Brotli needs net8.0, Zstandard needs net11.0.

Closes #2307
- Add RefitSettings.RequestCompressionOptions so a client can set the
  compressor's own knobs - window size, strategy, a Zstandard dictionary -
  which a CompressionLevel cannot express.
- Options set for a coding replace the level for that coding; the codings
  left unset still compress by level.
- The options types arrived with .NET 9.0, and Zstandard's with .NET 11.0,
  so the surface only exists from net9.0 onward.
@codecov

codecov Bot commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.91%. Comparing base (74cbb64) to head (f2df89a).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff            @@
##             main    #2311    +/-   ##
========================================
  Coverage   99.91%   99.91%            
========================================
  Files         192      194     +2     
  Lines       10027    10138   +111     
  Branches     1924     1947    +23     
========================================
+ Hits        10018    10129   +111     
  Partials        9        9            

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

- Add generator tests for the coding and level a [Body] parameter declares,
  including the None case that emits no call and a level declared without a
  coding.
- Add direct tests for the coding-to-token map and for the wrap and the
  compressor refusing a value that names no coding.
- Add options tests for Brotli and Zstandard, so every coding that accepts
  its own compressor settings is exercised on the targets that have them.
- Read an erroneous named argument through its TypedConstant kind rather
  than its value, so a value the compiler already rejected leaves the
  defaults in place instead of being read as an int.
@sonarqubecloud

Copy link
Copy Markdown

@glennawatson
glennawatson merged commit 1643323 into main Aug 18, 2026
18 checks passed
@glennawatson
glennawatson deleted the feat/request-body-compression branch August 18, 2026 13:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support new HttpContent wrappers from .NET 11 preview 7

2 participants