Skip to content

Add test: MaxResponseContentBufferSize enforced against decompressed content (Brotli)#127864

Open
Copilot wants to merge 5 commits intomainfrom
copilot/add-httpclient-decompression-test
Open

Add test: MaxResponseContentBufferSize enforced against decompressed content (Brotli)#127864
Copilot wants to merge 5 commits intomainfrom
copilot/add-httpclient-decompression-test

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 6, 2026

Adds a new System.Net.Http functional test to ensure HttpClient.MaxResponseContentBufferSize is enforced based on decompressed response size when automatic Brotli decompression is enabled, using the existing loopback server infrastructure in HttpClientHandlerTest.Decompression.cs.

Changes Made

  • Added a new Fact that streams highly-compressible Brotli content and asserts the client fails once the decompressed content exceeds MaxResponseContentBufferSize.
  • Track server-side written (compressed) bytes via a new ByteCountingStream wrapper (derived from DelegatingStream) to assert the server sent less compressed data than the decompressed buffer limit.
  • Added DelegatingStream.cs to System.Net.Http.WinHttpHandler.Functional.Tests.csproj (conditional on .NETCoreApp since it uses APIs unavailable on net481) to ensure ByteCountingStream can derive from DelegatingStream across all consuming test projects.
Original prompt

Add a test in dotnet/runtime that verifies HttpClient response buffering limits are enforced correctly with highly compressed data when automatic decompression is enabled.

Context:

  • Existing decompression tests live in src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Decompression.cs.
  • Existing MaxResponseContentBufferSize tests live in src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs, but they do not cover automatic decompression.
  • We previously looked for tests that verify MaxResponseContentBufferSize applies to decompressed content and did not find one.

Requested test behavior:

  • Add a new functional test that exercises automatic decompression with highly compressible data.
  • Example scenario: the server writes repeated 1 MB chunks of zeros into a BrotliStream using CompressionLevel.Optimal.
  • Configure the client with a response buffer limit of 10 MB.
  • Ensure the client fails once the decompressed content exceeds the configured response buffer limit.
  • The test should also verify that the server had to send only a relatively small amount of compressed data before the client gave up, i.e. assert that less data was actually sent on the server side than the decompressed amount.

Implementation guidance:

  • Prefer placing the test alongside existing decompression tests if that is the best fit.
  • Reuse existing loopback server / client test infrastructure already used by System.Net.Http functional tests.
  • Use automatic decompression on the client handler.
  • Use assertions appropriate for the existing test style in this repo, including validating the exception shape if applicable.
  • Keep the test reliable and bounded; it should demonstrate that highly compressed payloads do not require the server to send the full decompressed-size equivalent before the client enforces the limit.

Definition of done:

  • A new test is added to the repo.
  • The test validates buffer-limit enforcement against decompressed content for automatic decompression.
  • The test includes an assertion that the server sent less compressed data than the decompressed limit/total uncompressed amount, showing the client gives up quickly on highly compressed data.

The following is the prior conversation context from the user's chat exploration (may be truncated):

User: Looking through System.Net.Http tests, find ones where we're checking that MaxResponseContentBufferSize applies to decompressed contents (when using automatic decompression)

Assistant: I found no existing System.Net.Http test that explicitly checks that HttpClient.MaxResponseContentBufferSize is enforced against the decompressed response size when AutomaticDecompression is enabled.

Closest related tests are:

  • DecompressedResponse_MethodSpecified_DecompressedContentReturned
    in src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Decompression.cs
    It verifies automatic decompression returns the expected decompressed bytes, but does not involve MaxResponseContentBufferSize.
    URL:

    [Theory]
    [MemberData(nameof(DecompressedResponse_MethodSpecified_DecompressedContentReturned_MemberData))]
    [SkipOnPlatform(TestPlatforms.Browser, "AutomaticDecompression not supported on Browser")]
    public async Task DecompressedResponse_MethodSpecified_DecompressedContentReturned(string compressionName, bool all, bool useCopyTo, int contentLength)
    {
    if (IsWinHttpHandler &&
    (compressionName is "br" or "BR" or "zlib" or "ZLIB" or "zstd" or "ZSTD"))
    {
    // brotli, zlib, and zstd not supported on WinHttpHandler
    return;
    }
    Func<Stream, Stream> compress;
    DecompressionMethods methods;
    string encodingName = compressionName;
    switch (compressionName)
    {
    case "gzip":
    case "GZIP":
    compress = s => new GZipStream(s, CompressionLevel.Optimal, leaveOpen: true);
    methods = all ? DecompressionMethods.GZip : _all;
    break;
    #if !NETFRAMEWORK
    case "br":
    case "BR":
    compress = s => new BrotliStream(s, CompressionLevel.Optimal, leaveOpen: true);
    methods = all ? DecompressionMethods.Brotli : _all;
    break;
    case "zstd":
    case "ZSTD":
    compress = s => new ZstandardStream(s, CompressionLevel.Optimal, leaveOpen: true);
    methods = all ? DecompressionMethods.Zstandard : _all;
    break;
    case "zlib":
    case "ZLIB":
    compress = s => new ZLibStream(s, CompressionLevel.Optimal, leaveOpen: true);
    methods = all ? DecompressionMethods.Deflate : _all;
    encodingName = "deflate";
    break;
    #endif
    case "deflate":
    case "DEFLATE":
    compress = s => new DeflateStream(s, CompressionLevel.Optimal, leaveOpen: true);
    methods = all ? DecompressionMethods.Deflate : _all;
    break;
    default:
    throw new Exception($"Unexpected compression: {compressionName}");
    }
    var expectedContent = new byte[contentLength];
    new Random(42).NextBytes(expectedContent);
    await LoopbackServer.CreateClientAndServerAsync(async uri =>
    {
    using (HttpClientHandler handler = CreateHttpClientHandler())
    using (HttpClient client = CreateHttpClient(handler))
    {
    handler.AutomaticDecompression = methods;
    AssertExtensions.SequenceEqual(expectedContent, await client.GetByteArrayAsync(TestAsync, useCopyTo, uri));
    }
    }, async server =>
    {
    await server.AcceptConnectionAsync(async connection =>
    {
    await connection.ReadRequestHeaderAsync();
    await connection.WriteStringAsync($"HTTP/1.1 200 OK\r\nContent-Encoding: {encodingName}\r\n\r\n");
    using (Stream compressedStream = compress(connection.Stream))
    {
    await compressedStream.WriteAsync(expectedContent);
    }
    });
    });
    }

  • MaxResponseContentBufferSize_ThrowsIfTooSmallForContent
    in src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs
    It verifies buffer-size enforcement for normal content, but uses CustomResponseHandler / CustomContent, not automatic decompression.
    URL:

    [Theory]
    [InlineData(1, 2)]
    [InlineData(1, 127)]
    [InlineData(254, 255)]
    [InlineData(10, 256)]
    [InlineData(1, 440)]
    [InlineData(2, 1)]
    [InlineData(2, 2)]
    [InlineData(1000, 1000)]
    [InlineData(1000, int.MaxValue)]
    [InlineData(1000, int.MaxValue * 2L)]
    [InlineData(int.MaxValue, int.MaxValue + 1L)]
    public async Task MaxResponseContentBufferSize_ThrowsIfTooSmallForContent(long maxSize, long contentLength)
    {
    bool exceptionExpected = maxSize < contentLength;
    await TestAsync(client => client.GetStringAsync(CreateFakeUri()));
    await TestAsync(client => client.GetByteArrayAsync(CreateFakeUri()));
    await TestAsync(client => client.GetAsync(CreateFakeUri()));
    await TestAsync(client => client.PostAsync(CreateFakeUri(), new StringContent("foo")));
    await TestAsync(client => client.SendAsync(new HttpRequestMessage(HttpMethod.Get, CreateFakeUri())));
    await TestAsync(async client =>
    {
    using HttpResponseMessage response = await client.GetAsync(CreateFakeUri(), HttpCompletionOption.ResponseHeadersRead);
    await response.Content.LoadIntoBufferAsync(maxSize);
    });
    // Methods on HttpContent don't know about HttpClient's MaxResponseContentBufferSize, so they won't throw.
    exceptionExpected = false;
    if (contentLength > 1000)
    {
    // While the test would could with larger sizes, avoid allocating such buffers.
    return;
    }
    await TestAsync(async client =>
    {
    using HttpResponseMessage response = await client.GetAsync(CreateFakeUri(), HttpCompletionOption.ResponseHeadersRead);
    await response.Content.LoadIntoBufferAsync();
    });
    await TestAsync(async client =>
    {
    using HttpResponseMessage response = await client.GetAsync(CreateFakeUri(), HttpCompletionOption.ResponseHeadersRead);
    await response.Content.ReadAsStringAsync();
    });
    await TestAsync(async client =>
    {
    using HttpResponseMessage response = await client.GetAsync(CreateFakeUri(), HttpCompletionOption.ResponseHeadersRead);
    await response.Content.ReadAsByteArrayAsync();
    });
    await TestAsync(async client =>
    {
    using HttpResponseMessage response = await client.GetAsync(CreateFakeUri(), HttpCompletionOption.ResponseHeadersRead);
    using Stream stream = await response.Content.ReadAsStreamAsync();
    await stream.CopyToAsync(Stream.Null);
    });
    async Task TestAsync(Func<HttpClient, Task> clientAction)
    {
    foreach (bool setContentLength in BoolValues)
    {
    if (!setContentLength && contentLength > 1000)
    {
    // While the test could work with larger sizes, avoid allocating such buffers.
    continue;
    }
    bool wroteContent = false;
    var content = new CustomContent(async s =>
    {
    wroteContent = true;
    Assert.True(contentLength <= 1000);
    await s.WriteAsync(TestHelper.GenerateRandomContent((int)contentLength));
    });
    if (setContentLength)
    {
    content.Headers.ContentLength = contentLength;
    }
    var handler = new CustomResponseHandler((r, c) => Task.FromResult(new HttpResponseMessage() { Content = content }));
    using var client = new HttpClient(handler);
    client.MaxResponseContentBufferSize = maxSize;
    if (exceptionExpected)
    {
    HttpRequestException ex = await Assert.ThrowsAsync<HttpRequestException>(() => clientAction(client));
    Assert.Equal(HttpRequestError.ConfigurationLimitExceeded, ex.HttpRequestError);
    Assert.NotEqual(setContentLength, wroteContent);
    }
    else
    {
    await clientAction(client);
    Assert.True(wroteContent);
    }
    }
    }
    }

Relevant snippets:

await LoopbackServer.CreateClientAndServerAsync(async uri =>
{
    using (HttpClientHandler handler = CreateHttpClientHandler())
    using (HttpClient client = CreateHttpClient(handler))
    {
        handler.AutomaticDecompression = methods;
        AssertExtensions.SequenceEqual(expectedContent, await client.GetByteArrayAsync(TestAsync, useCopyTo, uri));
    }
}
public async Task MaxResponseContentBufferSize_ThrowsIfTooSmallForContent(long maxSize, long contentLength)
{
    bool exceptionExpected = maxSize < contentLength;

    await TestAsync(client => client.GetStringAsync(CreateFakeUri()));
    await TestAsync(client => client.GetByteArrayAsync(CreateFakeUri()));
    await TestAsync(client => client.GetAsync(CreateFakeUri()));
    await TestAsync(client => cl...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

*This pull request was created from Copilot chat.*
>

Copilot AI self-assigned this May 6, 2026
Copilot AI review requested due to automatic review settings May 6, 2026 13:01
Copilot AI review requested due to automatic review settings May 6, 2026 13:01
…compressed Brotli data

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/7f712e24-25ff-4525-a48f-e938eb01e97d

Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot May 6, 2026 13:26
Copilot AI changed the title [WIP] Add test for HttpClient response buffering with decompression Add test: MaxResponseContentBufferSize enforced against decompressed content (Brotli) May 6, 2026
Copilot AI requested a review from MihaZupan May 6, 2026 13:28
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @karelz, @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

@MihaZupan MihaZupan marked this pull request as ready for review May 6, 2026 18:41
Copilot AI review requested due to automatic review settings May 6, 2026 18:42
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new System.Net.Http functional test to ensure HttpClient.MaxResponseContentBufferSize is enforced based on decompressed response size when automatic Brotli decompression is enabled, using the existing loopback server infrastructure in HttpClientHandlerTest.Decompression.cs.

Changes:

  • Add a new Fact that streams highly-compressible Brotli content and asserts the client fails once the decompressed content exceeds MaxResponseContentBufferSize.
  • Track server-side written (compressed) bytes via a new ByteCountingStream wrapper (derived from DelegatingStream) to assert the server sent less compressed data than the decompressed buffer limit.
  • Add System.Threading import for CancellationToken used in the stream wrapper.

@MihaZupan
Copy link
Copy Markdown
Member

@copilot The build is failing after the last change to use DelegatingStream:

Build FAILED.

D:\a_work\1\s\src\libraries\Common\tests\System\Net\Http\HttpClientHandlerTest.Decompression.cs(351,51): error CS0246: The type or namespace name 'DelegatingStream' could not be found (are you missing a using directive or an assembly reference?) [D:\a_work\1\s\src\libraries\System.Net.Http.WinHttpHandler\tests\FunctionalTests\System.Net.Http.WinHttpHandler.Functional.Tests.csproj::TargetFramework=net11.0-windows] 0 Warning(s) 1 Error(s)

…ByteCountingStream

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/906885b0-b63f-4d08-99fc-6d4102a6439c

Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 6, 2026

@copilot The build is failing after the last change to use DelegatingStream:

Build FAILED.
...

Fixed in 91f345e — replaced DelegatingStream base class with a direct Stream derivation since DelegatingStream isn't included in the WinHttpHandler test project.

@MihaZupan
Copy link
Copy Markdown
Member

Fixed in 91f345e — replaced DelegatingStream base class with a direct Stream derivation since DelegatingStream isn't included in the WinHttpHandler test project.

@copilot Let's make sure it's included then. Using Delegating stream is better.

…yteCountingStream to use it

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/c140197c-72f8-42ea-9e8a-c34454989856

Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot May 6, 2026 20:27
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 6, 2026

Fixed in 91f345e — replaced DelegatingStream base class with a direct Stream derivation since DelegatingStream isn't inclu...

Done in 0d169e5 — added DelegatingStream.cs to the WinHttpHandler test project (conditionally for .NETCoreApp only, since it uses APIs unavailable on net481) and restored ByteCountingStream to derive from DelegatingStream.

@MihaZupan MihaZupan added this to the 11.0.0 milestone May 6, 2026
@MihaZupan MihaZupan requested review from a team and wfurt May 6, 2026 22:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants