From 4d78ef35a2669e9b04e68b0d1a69e0fff1b3da75 Mon Sep 17 00:00:00 2001 From: iremyux Date: Tue, 30 Jun 2026 16:31:51 +0200 Subject: [PATCH 01/21] Introduce CompressedContent class CompressionMethod enum --- .../System.Net.Http/ref/System.Net.Http.cs | 17 ++ .../ref/System.Net.Http.csproj | 1 + .../src/System.Net.Http.csproj | 2 + .../src/System/Net/Http/CompressedContent.cs | 153 ++++++++++++++++++ .../src/System/Net/Http/CompressionMethod.cs | 30 ++++ 5 files changed, 203 insertions(+) create mode 100644 src/libraries/System.Net.Http/src/System/Net/Http/CompressedContent.cs create mode 100644 src/libraries/System.Net.Http/src/System/Net/Http/CompressionMethod.cs diff --git a/src/libraries/System.Net.Http/ref/System.Net.Http.cs b/src/libraries/System.Net.Http/ref/System.Net.Http.cs index 2eb4200dc536d3..4bd554fbaccacd 100644 --- a/src/libraries/System.Net.Http/ref/System.Net.Http.cs +++ b/src/libraries/System.Net.Http/ref/System.Net.Http.cs @@ -17,6 +17,23 @@ protected override void SerializeToStream(System.IO.Stream stream, System.Net.Tr protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { throw null; } protected internal override bool TryComputeLength(out long length) { throw null; } } + public sealed partial class CompressedContent : System.Net.Http.HttpContent + { + public CompressedContent(System.Net.Http.HttpContent content, System.Net.Http.CompressionMethod method) { } + public CompressedContent(System.Net.Http.HttpContent content, System.Net.Http.CompressionMethod method, System.IO.Compression.CompressionLevel compressionLevel) { } + protected override void Dispose(bool disposing) { } + protected override void SerializeToStream(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { } + protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context) { throw null; } + protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { throw null; } + protected internal override bool TryComputeLength(out long length) { throw null; } + } + public enum CompressionMethod + { + GZip = 0, + Deflate = 1, + Brotli = 2, + Zstandard = 3, + } public enum ClientCertificateOption { Manual = 0, diff --git a/src/libraries/System.Net.Http/ref/System.Net.Http.csproj b/src/libraries/System.Net.Http/ref/System.Net.Http.csproj index 5c12a22894d477..4f098bcdb529ba 100644 --- a/src/libraries/System.Net.Http/ref/System.Net.Http.csproj +++ b/src/libraries/System.Net.Http/ref/System.Net.Http.csproj @@ -10,6 +10,7 @@ + diff --git a/src/libraries/System.Net.Http/src/System.Net.Http.csproj b/src/libraries/System.Net.Http/src/System.Net.Http.csproj index f973fa8499005b..a5d1573750ecd8 100644 --- a/src/libraries/System.Net.Http/src/System.Net.Http.csproj +++ b/src/libraries/System.Net.Http/src/System.Net.Http.csproj @@ -26,6 +26,8 @@ + + diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContent.cs new file mode 100644 index 00000000000000..36e75cce26f771 --- /dev/null +++ b/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContent.cs @@ -0,0 +1,153 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net.Http +{ + /// + /// Provides HTTP content that compresses the inner content using a specified encoding. + /// + public sealed class CompressedContent : HttpContent + { + private readonly HttpContent _originalContent; + private readonly CompressionMethod _method; + private readonly CompressionLevel _compressionLevel; + private bool _contentConsumed; + + /// + /// Initializes a new instance of that compresses the provided content + /// using the specified compression method with . + /// + /// The HTTP content to compress. + /// The compression method to use. + public CompressedContent(HttpContent content, CompressionMethod method) + : this(content, method, CompressionLevel.Optimal) + { + } + + /// + /// Initializes a new instance of that compresses the provided content + /// using the specified compression method and compression level. + /// + /// The HTTP content to compress. + /// The compression method to use. + /// The level of compression to use. + public CompressedContent(HttpContent content, CompressionMethod method, CompressionLevel compressionLevel) + { + ArgumentNullException.ThrowIfNull(content); + + // Resolve the wire encoding name, which also validates that the method is a defined value. + string encoding = GetEncodingName(method); + + _originalContent = content; + _method = method; + _compressionLevel = compressionLevel; + + // Copy headers from the original content. + foreach (KeyValuePair> header in content.Headers) + { + Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + // Append our encoding to the Content-Encoding header (supports stacking per HTTP spec). + Headers.ContentEncoding.Add(encoding); + + // Remove Content-Length since we don't know the compressed size upfront. + Headers.ContentLength = null; + } + + protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) + { + ThrowIfContentConsumed(); + + using Stream compressionStream = CreateCompressionStream(stream); + _originalContent.CopyTo(compressionStream, context, cancellationToken); + } + + protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) => + SerializeToStreamAsync(stream, context, CancellationToken.None); + + protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) + { + ThrowIfContentConsumed(); + + Stream compressionStream = CreateCompressionStream(stream); + await using (compressionStream.ConfigureAwait(false)) + { + await _originalContent.CopyToAsync(compressionStream, cancellationToken).ConfigureAwait(false); + } + } + + protected internal override bool TryComputeLength(out long length) + { + // Compressed size is not known without actually compressing. + length = 0; + return false; + } + + internal override bool AllowDuplex => false; + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _originalContent.Dispose(); + } + + base.Dispose(disposing); + } + + private Stream CreateCompressionStream(Stream outputStream) + { + switch (_method) + { + case CompressionMethod.GZip: + return new GZipStream(outputStream, _compressionLevel, leaveOpen: true); + + case CompressionMethod.Deflate: + return new ZLibStream(outputStream, _compressionLevel, leaveOpen: true); + + case CompressionMethod.Brotli: + if (OperatingSystem.IsBrowser() || OperatingSystem.IsWasi()) + { + throw new PlatformNotSupportedException(); + } + return new BrotliStream(outputStream, _compressionLevel, leaveOpen: true); + + case CompressionMethod.Zstandard: + if (OperatingSystem.IsBrowser() || OperatingSystem.IsWasi()) + { + throw new PlatformNotSupportedException(); + } + return new ZstandardStream(outputStream, _compressionLevel, leaveOpen: true); + + default: + throw new ArgumentOutOfRangeException(nameof(_method)); + } + } + + private void ThrowIfContentConsumed() + { + if (_contentConsumed) + { + throw new InvalidOperationException(SR.net_http_content_stream_already_read); + } + + _contentConsumed = true; + } + + private static string GetEncodingName(CompressionMethod method) => method switch + { + CompressionMethod.GZip => "gzip", + CompressionMethod.Deflate => "deflate", + CompressionMethod.Brotli => "br", + CompressionMethod.Zstandard => "zstd", + _ => throw new ArgumentOutOfRangeException(nameof(method)) + }; + } +} diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/CompressionMethod.cs b/src/libraries/System.Net.Http/src/System/Net/Http/CompressionMethod.cs new file mode 100644 index 00000000000000..12068577712be5 --- /dev/null +++ b/src/libraries/System.Net.Http/src/System/Net/Http/CompressionMethod.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Net.Http; + +/// +/// Specifies the compression method used by to compress request content. +/// +public enum CompressionMethod +{ + /// + /// GZip compression, corresponding to the gzip content coding. + /// + GZip, + + /// + /// Deflate (zlib) compression, corresponding to the deflate content coding. + /// + Deflate, + + /// + /// Brotli compression, corresponding to the br content coding. + /// + Brotli, + + /// + /// Zstandard compression, corresponding to the zstd content coding. + /// + Zstandard, +} From c1bfd59b8e933ed80c48c3a4b1d893b3594aef84 Mon Sep 17 00:00:00 2001 From: iremyux Date: Tue, 30 Jun 2026 17:08:46 +0200 Subject: [PATCH 02/21] Change the enum values to match DecompressionMethods --- .../src/System/Net/Http/CompressionMethod.cs | 23 ++++--------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/CompressionMethod.cs b/src/libraries/System.Net.Http/src/System/Net/Http/CompressionMethod.cs index 12068577712be5..3ff8434a61ad74 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/CompressionMethod.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/CompressionMethod.cs @@ -8,23 +8,8 @@ namespace System.Net.Http; /// public enum CompressionMethod { - /// - /// GZip compression, corresponding to the gzip content coding. - /// - GZip, - - /// - /// Deflate (zlib) compression, corresponding to the deflate content coding. - /// - Deflate, - - /// - /// Brotli compression, corresponding to the br content coding. - /// - Brotli, - - /// - /// Zstandard compression, corresponding to the zstd content coding. - /// - Zstandard, + GZip = 0x1, + Deflate = 0x2, + Brotli = 0x4, + Zstandard = 0x8 } From 8c994174797ef21633536622202f6882841113b6 Mon Sep 17 00:00:00 2001 From: iremyux Date: Tue, 30 Jun 2026 17:19:48 +0200 Subject: [PATCH 03/21] Change the enum values to match DecompressionMethods --- src/libraries/System.Net.Http/ref/System.Net.Http.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Net.Http/ref/System.Net.Http.cs b/src/libraries/System.Net.Http/ref/System.Net.Http.cs index 4bd554fbaccacd..f98bd2b8b8d28a 100644 --- a/src/libraries/System.Net.Http/ref/System.Net.Http.cs +++ b/src/libraries/System.Net.Http/ref/System.Net.Http.cs @@ -29,10 +29,10 @@ protected override void SerializeToStream(System.IO.Stream stream, System.Net.Tr } public enum CompressionMethod { - GZip = 0, - Deflate = 1, - Brotli = 2, - Zstandard = 3, + GZip = 1, + Deflate = 2, + Brotli = 4, + Zstandard = 8, } public enum ClientCertificateOption { From e5734857ab516d12c93e2a315adfee528f837d69 Mon Sep 17 00:00:00 2001 From: iremyux Date: Wed, 1 Jul 2026 14:58:16 +0200 Subject: [PATCH 04/21] Apply API changes --- .../System.Net.Http/ref/System.Net.Http.cs | 29 +++- .../ref/System.Net.Http.csproj | 1 + .../src/System.Net.Http.csproj | 6 +- .../Net/Http/BrotliCompressedContent.cs | 90 +++++++++++ .../src/System/Net/Http/CompressedContent.cs | 153 ------------------ .../System/Net/Http/CompressedContentCore.cs | 73 +++++++++ .../src/System/Net/Http/CompressionMethod.cs | 15 -- .../System/Net/Http/GZipCompressedContent.cs | 83 ++++++++++ .../Net/Http/ZstandardCompressedContent.cs | 90 +++++++++++ 9 files changed, 362 insertions(+), 178 deletions(-) create mode 100644 src/libraries/System.Net.Http/src/System/Net/Http/BrotliCompressedContent.cs delete mode 100644 src/libraries/System.Net.Http/src/System/Net/Http/CompressedContent.cs create mode 100644 src/libraries/System.Net.Http/src/System/Net/Http/CompressedContentCore.cs delete mode 100644 src/libraries/System.Net.Http/src/System/Net/Http/CompressionMethod.cs create mode 100644 src/libraries/System.Net.Http/src/System/Net/Http/GZipCompressedContent.cs create mode 100644 src/libraries/System.Net.Http/src/System/Net/Http/ZstandardCompressedContent.cs diff --git a/src/libraries/System.Net.Http/ref/System.Net.Http.cs b/src/libraries/System.Net.Http/ref/System.Net.Http.cs index f98bd2b8b8d28a..a90b30c3892cc0 100644 --- a/src/libraries/System.Net.Http/ref/System.Net.Http.cs +++ b/src/libraries/System.Net.Http/ref/System.Net.Http.cs @@ -17,22 +17,35 @@ protected override void SerializeToStream(System.IO.Stream stream, System.Net.Tr protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { throw null; } protected internal override bool TryComputeLength(out long length) { throw null; } } - public sealed partial class CompressedContent : System.Net.Http.HttpContent + public sealed partial class BrotliCompressedContent : System.Net.Http.HttpContent { - public CompressedContent(System.Net.Http.HttpContent content, System.Net.Http.CompressionMethod method) { } - public CompressedContent(System.Net.Http.HttpContent content, System.Net.Http.CompressionMethod method, System.IO.Compression.CompressionLevel compressionLevel) { } + public BrotliCompressedContent(System.Net.Http.HttpContent content) { } + public BrotliCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.BrotliCompressionOptions compressionOptions) { } protected override void Dispose(bool disposing) { } protected override void SerializeToStream(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { } protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context) { throw null; } protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { throw null; } protected internal override bool TryComputeLength(out long length) { throw null; } } - public enum CompressionMethod + public sealed partial class GZipCompressedContent : System.Net.Http.HttpContent { - GZip = 1, - Deflate = 2, - Brotli = 4, - Zstandard = 8, + public GZipCompressedContent(System.Net.Http.HttpContent content) { } + public GZipCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.ZLibCompressionOptions compressionOptions) { } + protected override void Dispose(bool disposing) { } + protected override void SerializeToStream(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { } + protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context) { throw null; } + protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { throw null; } + protected internal override bool TryComputeLength(out long length) { throw null; } + } + public sealed partial class ZstandardCompressedContent : System.Net.Http.HttpContent + { + public ZstandardCompressedContent(System.Net.Http.HttpContent content) { } + public ZstandardCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.ZstandardCompressionOptions compressionOptions) { } + protected override void Dispose(bool disposing) { } + protected override void SerializeToStream(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { } + protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context) { throw null; } + protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { throw null; } + protected internal override bool TryComputeLength(out long length) { throw null; } } public enum ClientCertificateOption { diff --git a/src/libraries/System.Net.Http/ref/System.Net.Http.csproj b/src/libraries/System.Net.Http/ref/System.Net.Http.csproj index 4f098bcdb529ba..b4ff186f10ece5 100644 --- a/src/libraries/System.Net.Http/ref/System.Net.Http.csproj +++ b/src/libraries/System.Net.Http/ref/System.Net.Http.csproj @@ -11,6 +11,7 @@ + diff --git a/src/libraries/System.Net.Http/src/System.Net.Http.csproj b/src/libraries/System.Net.Http/src/System.Net.Http.csproj index a5d1573750ecd8..ee798fe89bdb51 100644 --- a/src/libraries/System.Net.Http/src/System.Net.Http.csproj +++ b/src/libraries/System.Net.Http/src/System.Net.Http.csproj @@ -26,8 +26,10 @@ - - + + + + diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/BrotliCompressedContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/BrotliCompressedContent.cs new file mode 100644 index 00000000000000..30565f793fc838 --- /dev/null +++ b/src/libraries/System.Net.Http/src/System/Net/Http/BrotliCompressedContent.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.IO.Compression; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net.Http +{ + /// + /// Provides HTTP content that compresses the inner content using the Brotli content coding. + /// + public sealed class BrotliCompressedContent : HttpContent + { + private const string Encoding = "br"; + + private readonly CompressedContentCore _core; + private readonly BrotliCompressionOptions? _compressionOptions; + + /// + /// Initializes a new instance of the class that compresses the + /// provided content using the Brotli content coding with default compression settings. + /// + /// The HTTP content to compress. + public BrotliCompressedContent(HttpContent content) + { + ArgumentNullException.ThrowIfNull(content); + + _core = new CompressedContentCore(content, CreateCompressionStream); + CompressedContentCore.InitializeHeaders(this, content, Encoding); + } + + /// + /// Initializes a new instance of the class that compresses the + /// provided content using the Brotli content coding and the specified compression options. + /// + /// The HTTP content to compress. + /// The options used to fine-tune the compression. + public BrotliCompressedContent(HttpContent content, BrotliCompressionOptions compressionOptions) + { + ArgumentNullException.ThrowIfNull(content); + ArgumentNullException.ThrowIfNull(compressionOptions); + + _compressionOptions = compressionOptions; + _core = new CompressedContentCore(content, CreateCompressionStream); + CompressedContentCore.InitializeHeaders(this, content, Encoding); + } + + protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) => + _core.SerializeToStream(stream, context, cancellationToken); + + protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) => + _core.SerializeToStreamAsync(stream, CancellationToken.None); + + protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) => + _core.SerializeToStreamAsync(stream, cancellationToken); + + protected internal override bool TryComputeLength(out long length) + { + // Compressed size is not known without actually compressing. + length = 0; + return false; + } + + internal override bool AllowDuplex => false; + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _core.Dispose(); + } + + base.Dispose(disposing); + } + + private BrotliStream CreateCompressionStream(Stream outputStream) + { + if (OperatingSystem.IsBrowser() || OperatingSystem.IsWasi()) + { + throw new PlatformNotSupportedException(); + } + + return _compressionOptions is null + ? new BrotliStream(outputStream, CompressionLevel.Optimal, leaveOpen: true) + : new BrotliStream(outputStream, _compressionOptions, leaveOpen: true); + } + } +} diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContent.cs deleted file mode 100644 index 36e75cce26f771..00000000000000 --- a/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContent.cs +++ /dev/null @@ -1,153 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.IO; -using System.IO.Compression; -using System.Threading; -using System.Threading.Tasks; - -namespace System.Net.Http -{ - /// - /// Provides HTTP content that compresses the inner content using a specified encoding. - /// - public sealed class CompressedContent : HttpContent - { - private readonly HttpContent _originalContent; - private readonly CompressionMethod _method; - private readonly CompressionLevel _compressionLevel; - private bool _contentConsumed; - - /// - /// Initializes a new instance of that compresses the provided content - /// using the specified compression method with . - /// - /// The HTTP content to compress. - /// The compression method to use. - public CompressedContent(HttpContent content, CompressionMethod method) - : this(content, method, CompressionLevel.Optimal) - { - } - - /// - /// Initializes a new instance of that compresses the provided content - /// using the specified compression method and compression level. - /// - /// The HTTP content to compress. - /// The compression method to use. - /// The level of compression to use. - public CompressedContent(HttpContent content, CompressionMethod method, CompressionLevel compressionLevel) - { - ArgumentNullException.ThrowIfNull(content); - - // Resolve the wire encoding name, which also validates that the method is a defined value. - string encoding = GetEncodingName(method); - - _originalContent = content; - _method = method; - _compressionLevel = compressionLevel; - - // Copy headers from the original content. - foreach (KeyValuePair> header in content.Headers) - { - Headers.TryAddWithoutValidation(header.Key, header.Value); - } - - // Append our encoding to the Content-Encoding header (supports stacking per HTTP spec). - Headers.ContentEncoding.Add(encoding); - - // Remove Content-Length since we don't know the compressed size upfront. - Headers.ContentLength = null; - } - - protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) - { - ThrowIfContentConsumed(); - - using Stream compressionStream = CreateCompressionStream(stream); - _originalContent.CopyTo(compressionStream, context, cancellationToken); - } - - protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) => - SerializeToStreamAsync(stream, context, CancellationToken.None); - - protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) - { - ThrowIfContentConsumed(); - - Stream compressionStream = CreateCompressionStream(stream); - await using (compressionStream.ConfigureAwait(false)) - { - await _originalContent.CopyToAsync(compressionStream, cancellationToken).ConfigureAwait(false); - } - } - - protected internal override bool TryComputeLength(out long length) - { - // Compressed size is not known without actually compressing. - length = 0; - return false; - } - - internal override bool AllowDuplex => false; - - protected override void Dispose(bool disposing) - { - if (disposing) - { - _originalContent.Dispose(); - } - - base.Dispose(disposing); - } - - private Stream CreateCompressionStream(Stream outputStream) - { - switch (_method) - { - case CompressionMethod.GZip: - return new GZipStream(outputStream, _compressionLevel, leaveOpen: true); - - case CompressionMethod.Deflate: - return new ZLibStream(outputStream, _compressionLevel, leaveOpen: true); - - case CompressionMethod.Brotli: - if (OperatingSystem.IsBrowser() || OperatingSystem.IsWasi()) - { - throw new PlatformNotSupportedException(); - } - return new BrotliStream(outputStream, _compressionLevel, leaveOpen: true); - - case CompressionMethod.Zstandard: - if (OperatingSystem.IsBrowser() || OperatingSystem.IsWasi()) - { - throw new PlatformNotSupportedException(); - } - return new ZstandardStream(outputStream, _compressionLevel, leaveOpen: true); - - default: - throw new ArgumentOutOfRangeException(nameof(_method)); - } - } - - private void ThrowIfContentConsumed() - { - if (_contentConsumed) - { - throw new InvalidOperationException(SR.net_http_content_stream_already_read); - } - - _contentConsumed = true; - } - - private static string GetEncodingName(CompressionMethod method) => method switch - { - CompressionMethod.GZip => "gzip", - CompressionMethod.Deflate => "deflate", - CompressionMethod.Brotli => "br", - CompressionMethod.Zstandard => "zstd", - _ => throw new ArgumentOutOfRangeException(nameof(method)) - }; - } -} diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContentCore.cs b/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContentCore.cs new file mode 100644 index 00000000000000..6ccecb8cbec580 --- /dev/null +++ b/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContentCore.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net.Http +{ + /// + /// Shared implementation for the per-algorithm compressed content types. Holds the inner content and + /// drives serialization through a caller-provided factory that wraps the output stream in a compression stream. + /// + internal sealed class CompressedContentCore + { + private readonly HttpContent _originalContent; + private readonly Func _createCompressionStream; + private bool _contentConsumed; + + public CompressedContentCore(HttpContent originalContent, Func createCompressionStream) + { + _originalContent = originalContent; + _createCompressionStream = createCompressionStream; + } + + public static void InitializeHeaders(HttpContent target, HttpContent source, string encoding) + { + // Copy headers from the original content. + foreach (KeyValuePair> header in source.Headers) + { + target.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + // Append our encoding to the Content-Encoding header (supports stacking per HTTP spec). + target.Headers.ContentEncoding.Add(encoding); + + // Remove Content-Length since we don't know the compressed size upfront. + target.Headers.ContentLength = null; + } + + public void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) + { + ThrowIfContentConsumed(); + + using Stream compressionStream = _createCompressionStream(stream); + _originalContent.CopyTo(compressionStream, context, cancellationToken); + } + + public async Task SerializeToStreamAsync(Stream stream, CancellationToken cancellationToken) + { + ThrowIfContentConsumed(); + + Stream compressionStream = _createCompressionStream(stream); + await using (compressionStream.ConfigureAwait(false)) + { + await _originalContent.CopyToAsync(compressionStream, cancellationToken).ConfigureAwait(false); + } + } + + public void Dispose() => _originalContent.Dispose(); + + private void ThrowIfContentConsumed() + { + if (_contentConsumed) + { + throw new InvalidOperationException(SR.net_http_content_stream_already_read); + } + + _contentConsumed = true; + } + } +} diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/CompressionMethod.cs b/src/libraries/System.Net.Http/src/System/Net/Http/CompressionMethod.cs deleted file mode 100644 index 3ff8434a61ad74..00000000000000 --- a/src/libraries/System.Net.Http/src/System/Net/Http/CompressionMethod.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace System.Net.Http; - -/// -/// Specifies the compression method used by to compress request content. -/// -public enum CompressionMethod -{ - GZip = 0x1, - Deflate = 0x2, - Brotli = 0x4, - Zstandard = 0x8 -} diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/GZipCompressedContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/GZipCompressedContent.cs new file mode 100644 index 00000000000000..a3b97e83200c41 --- /dev/null +++ b/src/libraries/System.Net.Http/src/System/Net/Http/GZipCompressedContent.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.IO.Compression; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net.Http +{ + /// + /// Provides HTTP content that compresses the inner content using the gzip content coding. + /// + public sealed class GZipCompressedContent : HttpContent + { + private const string Encoding = "gzip"; + + private readonly CompressedContentCore _core; + private readonly ZLibCompressionOptions? _compressionOptions; + + /// + /// Initializes a new instance of the class that compresses the + /// provided content using the gzip content coding with default compression settings. + /// + /// The HTTP content to compress. + public GZipCompressedContent(HttpContent content) + { + ArgumentNullException.ThrowIfNull(content); + + _core = new CompressedContentCore(content, CreateCompressionStream); + CompressedContentCore.InitializeHeaders(this, content, Encoding); + } + + /// + /// Initializes a new instance of the class that compresses the + /// provided content using the gzip content coding and the specified compression options. + /// + /// The HTTP content to compress. + /// The options used to fine-tune the compression. + public GZipCompressedContent(HttpContent content, ZLibCompressionOptions compressionOptions) + { + ArgumentNullException.ThrowIfNull(content); + ArgumentNullException.ThrowIfNull(compressionOptions); + + _compressionOptions = compressionOptions; + _core = new CompressedContentCore(content, CreateCompressionStream); + CompressedContentCore.InitializeHeaders(this, content, Encoding); + } + + protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) => + _core.SerializeToStream(stream, context, cancellationToken); + + protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) => + _core.SerializeToStreamAsync(stream, CancellationToken.None); + + protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) => + _core.SerializeToStreamAsync(stream, cancellationToken); + + protected internal override bool TryComputeLength(out long length) + { + // Compressed size is not known without actually compressing. + length = 0; + return false; + } + + internal override bool AllowDuplex => false; + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _core.Dispose(); + } + + base.Dispose(disposing); + } + + private GZipStream CreateCompressionStream(Stream outputStream) => + _compressionOptions is null + ? new GZipStream(outputStream, CompressionLevel.Optimal, leaveOpen: true) + : new GZipStream(outputStream, _compressionOptions, leaveOpen: true); + } +} diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/ZstandardCompressedContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/ZstandardCompressedContent.cs new file mode 100644 index 00000000000000..66c222dc5ed567 --- /dev/null +++ b/src/libraries/System.Net.Http/src/System/Net/Http/ZstandardCompressedContent.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.IO.Compression; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net.Http +{ + /// + /// Provides HTTP content that compresses the inner content using the Zstandard content coding. + /// + public sealed class ZstandardCompressedContent : HttpContent + { + private const string Encoding = "zstd"; + + private readonly CompressedContentCore _core; + private readonly ZstandardCompressionOptions? _compressionOptions; + + /// + /// Initializes a new instance of the class that compresses the + /// provided content using the Zstandard content coding with default compression settings. + /// + /// The HTTP content to compress. + public ZstandardCompressedContent(HttpContent content) + { + ArgumentNullException.ThrowIfNull(content); + + _core = new CompressedContentCore(content, CreateCompressionStream); + CompressedContentCore.InitializeHeaders(this, content, Encoding); + } + + /// + /// Initializes a new instance of the class that compresses the + /// provided content using the Zstandard content coding and the specified compression options. + /// + /// The HTTP content to compress. + /// The options used to fine-tune the compression. + public ZstandardCompressedContent(HttpContent content, ZstandardCompressionOptions compressionOptions) + { + ArgumentNullException.ThrowIfNull(content); + ArgumentNullException.ThrowIfNull(compressionOptions); + + _compressionOptions = compressionOptions; + _core = new CompressedContentCore(content, CreateCompressionStream); + CompressedContentCore.InitializeHeaders(this, content, Encoding); + } + + protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) => + _core.SerializeToStream(stream, context, cancellationToken); + + protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) => + _core.SerializeToStreamAsync(stream, CancellationToken.None); + + protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) => + _core.SerializeToStreamAsync(stream, cancellationToken); + + protected internal override bool TryComputeLength(out long length) + { + // Compressed size is not known without actually compressing. + length = 0; + return false; + } + + internal override bool AllowDuplex => false; + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _core.Dispose(); + } + + base.Dispose(disposing); + } + + private ZstandardStream CreateCompressionStream(Stream outputStream) + { + if (OperatingSystem.IsBrowser() || OperatingSystem.IsWasi()) + { + throw new PlatformNotSupportedException(); + } + + return _compressionOptions is null + ? new ZstandardStream(outputStream, CompressionLevel.Optimal, leaveOpen: true) + : new ZstandardStream(outputStream, _compressionOptions, leaveOpen: true); + } + } +} From 2227ece04e2355d53bb6f7154a6091330eb40c91 Mon Sep 17 00:00:00 2001 From: iremyux Date: Tue, 7 Jul 2026 13:04:24 +0200 Subject: [PATCH 05/21] Remove _contentConsumed check --- .../src/System/Net/Http/CompressedContentCore.cs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContentCore.cs b/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContentCore.cs index 6ccecb8cbec580..51f8ef9b98c913 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContentCore.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContentCore.cs @@ -16,7 +16,6 @@ internal sealed class CompressedContentCore { private readonly HttpContent _originalContent; private readonly Func _createCompressionStream; - private bool _contentConsumed; public CompressedContentCore(HttpContent originalContent, Func createCompressionStream) { @@ -41,16 +40,12 @@ public static void InitializeHeaders(HttpContent target, HttpContent source, str public void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) { - ThrowIfContentConsumed(); - using Stream compressionStream = _createCompressionStream(stream); _originalContent.CopyTo(compressionStream, context, cancellationToken); } public async Task SerializeToStreamAsync(Stream stream, CancellationToken cancellationToken) { - ThrowIfContentConsumed(); - Stream compressionStream = _createCompressionStream(stream); await using (compressionStream.ConfigureAwait(false)) { @@ -59,15 +54,5 @@ public async Task SerializeToStreamAsync(Stream stream, CancellationToken cancel } public void Dispose() => _originalContent.Dispose(); - - private void ThrowIfContentConsumed() - { - if (_contentConsumed) - { - throw new InvalidOperationException(SR.net_http_content_stream_already_read); - } - - _contentConsumed = true; - } } } From 887e18a4fa70ea530bca1d6c1a34b7972c50ad1f Mon Sep 17 00:00:00 2001 From: iremyux Date: Tue, 7 Jul 2026 13:08:50 +0200 Subject: [PATCH 06/21] Optimize Content-Encoding header append --- .../System/Net/Http/CompressedContentCore.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContentCore.cs b/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContentCore.cs index 51f8ef9b98c913..fb1c55973a8cd5 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContentCore.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContentCore.cs @@ -1,8 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Generic; using System.IO; +using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; @@ -26,13 +26,19 @@ public CompressedContentCore(HttpContent originalContent, Func c public static void InitializeHeaders(HttpContent target, HttpContent source, string encoding) { // Copy headers from the original content. - foreach (KeyValuePair> header in source.Headers) - { - target.Headers.TryAddWithoutValidation(header.Key, header.Value); - } + target.Headers.AddHeaders(source.Headers); // Append our encoding to the Content-Encoding header (supports stacking per HTTP spec). - target.Headers.ContentEncoding.Add(encoding); + // The common case is that the inner content has no Content-Encoding, so take the + // allocation-free fast path and only materialize the collection when we need to stack. + if (target.Headers.Contains(KnownHeaders.ContentEncoding.Descriptor)) + { + target.Headers.ContentEncoding.Add(encoding); + } + else + { + target.Headers.TryAddWithoutValidation(KnownHeaders.ContentEncoding.Descriptor, encoding); + } // Remove Content-Length since we don't know the compressed size upfront. target.Headers.ContentLength = null; From 5ed6d1ad84bd7e04fc1c0f480ee90ecd314fae01 Mon Sep 17 00:00:00 2001 From: iremyux Date: Tue, 7 Jul 2026 13:38:24 +0200 Subject: [PATCH 07/21] Annotate Zstd and BrotliCompressedContent as unsupported on browser and wasi --- src/libraries/System.Net.Http/ref/System.Net.Http.cs | 4 ++++ .../src/System/Net/Http/BrotliCompressedContent.cs | 3 +++ .../src/System/Net/Http/ZstandardCompressedContent.cs | 3 +++ 3 files changed, 10 insertions(+) diff --git a/src/libraries/System.Net.Http/ref/System.Net.Http.cs b/src/libraries/System.Net.Http/ref/System.Net.Http.cs index a90b30c3892cc0..72e5834f0b4d69 100644 --- a/src/libraries/System.Net.Http/ref/System.Net.Http.cs +++ b/src/libraries/System.Net.Http/ref/System.Net.Http.cs @@ -17,6 +17,8 @@ protected override void SerializeToStream(System.IO.Stream stream, System.Net.Tr protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { throw null; } protected internal override bool TryComputeLength(out long length) { throw null; } } + [System.Runtime.Versioning.UnsupportedOSPlatform("browser")] + [System.Runtime.Versioning.UnsupportedOSPlatform("wasi")] public sealed partial class BrotliCompressedContent : System.Net.Http.HttpContent { public BrotliCompressedContent(System.Net.Http.HttpContent content) { } @@ -37,6 +39,8 @@ protected override void SerializeToStream(System.IO.Stream stream, System.Net.Tr protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { throw null; } protected internal override bool TryComputeLength(out long length) { throw null; } } + [System.Runtime.Versioning.UnsupportedOSPlatform("browser")] + [System.Runtime.Versioning.UnsupportedOSPlatform("wasi")] public sealed partial class ZstandardCompressedContent : System.Net.Http.HttpContent { public ZstandardCompressedContent(System.Net.Http.HttpContent content) { } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/BrotliCompressedContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/BrotliCompressedContent.cs index 30565f793fc838..16b43387385100 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/BrotliCompressedContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/BrotliCompressedContent.cs @@ -3,6 +3,7 @@ using System.IO; using System.IO.Compression; +using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; @@ -11,6 +12,8 @@ namespace System.Net.Http /// /// Provides HTTP content that compresses the inner content using the Brotli content coding. /// + [UnsupportedOSPlatform("browser")] + [UnsupportedOSPlatform("wasi")] public sealed class BrotliCompressedContent : HttpContent { private const string Encoding = "br"; diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/ZstandardCompressedContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/ZstandardCompressedContent.cs index 66c222dc5ed567..68b78d29bc3225 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/ZstandardCompressedContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/ZstandardCompressedContent.cs @@ -3,6 +3,7 @@ using System.IO; using System.IO.Compression; +using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; @@ -11,6 +12,8 @@ namespace System.Net.Http /// /// Provides HTTP content that compresses the inner content using the Zstandard content coding. /// + [UnsupportedOSPlatform("browser")] + [UnsupportedOSPlatform("wasi")] public sealed class ZstandardCompressedContent : HttpContent { private const string Encoding = "zstd"; From f4c002a8f7f3dc551d4e363c0a256ced8c34f696 Mon Sep 17 00:00:00 2001 From: iremyux Date: Tue, 7 Jul 2026 15:15:13 +0200 Subject: [PATCH 08/21] Add tests --- .../FunctionalTests/CompressedContentTest.cs | 245 ++++++++++++++++++ .../System.Net.Http.Functional.Tests.csproj | 1 + 2 files changed, 246 insertions(+) create mode 100644 src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.cs diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.cs new file mode 100644 index 00000000000000..342751d26a9aff --- /dev/null +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.cs @@ -0,0 +1,245 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using Xunit; + +namespace System.Net.Http.Functional.Tests +{ + public class CompressedContentTest + { + [Fact] + public void Ctor_NullContent_ThrowsArgumentNullException() + { + AssertExtensions.Throws("content", () => new GZipCompressedContent(null)); + AssertExtensions.Throws("content", () => new GZipCompressedContent(null, new ZLibCompressionOptions())); + AssertExtensions.Throws("content", () => new BrotliCompressedContent(null)); + AssertExtensions.Throws("content", () => new BrotliCompressedContent(null, new BrotliCompressionOptions())); + AssertExtensions.Throws("content", () => new ZstandardCompressedContent(null)); + AssertExtensions.Throws("content", () => new ZstandardCompressedContent(null, new ZstandardCompressionOptions())); + } + + [Fact] + public void Ctor_NullOptions_ThrowsArgumentNullException() + { + var inner = new ByteArrayContent(Array.Empty()); + AssertExtensions.Throws("compressionOptions", () => new GZipCompressedContent(inner, null)); + AssertExtensions.Throws("compressionOptions", () => new BrotliCompressedContent(inner, null)); + AssertExtensions.Throws("compressionOptions", () => new ZstandardCompressedContent(inner, null)); + } + + [Theory] + [InlineData("gzip")] + [InlineData("br")] + [InlineData("zstd")] + public void Ctor_SetsContentEncodingHeader(string encoding) + { + HttpContent content = CreateContent(encoding, new ByteArrayContent(Array.Empty())); + + Assert.Equal(new[] { encoding }, content.Headers.ContentEncoding); + } + + [Fact] + public void Ctor_RemovesContentLength() + { + var inner = new ByteArrayContent(new byte[10]); + Assert.Equal(10, inner.Headers.ContentLength); + + var content = new GZipCompressedContent(inner); + + Assert.Null(content.Headers.ContentLength); + } + + [Fact] + public void Ctor_CopiesInnerContentHeaders() + { + var inner = new ByteArrayContent(Array.Empty()); + inner.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + + var content = new GZipCompressedContent(inner); + + Assert.Equal("application/json", content.Headers.ContentType.MediaType); + } + + [Fact] + public void Ctor_NestedContent_StacksContentEncoding() + { + var inner = new GZipCompressedContent(new ByteArrayContent(Array.Empty())); + var outer = new BrotliCompressedContent(inner); + + Assert.Equal(new[] { "gzip", "br" }, outer.Headers.ContentEncoding); + } + + [Fact] + public void Dispose_DisposesInnerContent() + { + var inner = new MockContent(); + var content = new GZipCompressedContent(inner); + + content.Dispose(); + + Assert.Equal(1, inner.DisposeCount); + } + + [Theory] + [MemberData(nameof(RoundTripData))] + public async Task SerializeToStream_RoundTrips_MatchesOriginal(string encoding, bool async) + { + byte[] original = Encoding.UTF8.GetBytes("The quick brown fox jumps over the lazy dog. " + new string('a', 1024)); + HttpContent content = CreateContent(encoding, new ByteArrayContent(original)); + + byte[] compressed = await SerializeAsync(content, async); + byte[] decompressed = Decompress(compressed, encoding); + + Assert.Equal(original, decompressed); + } + + [Fact] + public async Task SerializeToStream_WithOptions_RoundTrips() + { + byte[] original = Encoding.UTF8.GetBytes(new string('a', 4096)); + + var gzip = new GZipCompressedContent(new ByteArrayContent(original), new ZLibCompressionOptions { CompressionLevel = 5 }); + Assert.Equal(original, Decompress(await SerializeAsync(gzip, async: true), "gzip")); + + if (!OperatingSystem.IsBrowser() && !OperatingSystem.IsWasi()) + { + var brotli = new BrotliCompressedContent(new ByteArrayContent(original), new BrotliCompressionOptions { Quality = 5 }); + Assert.Equal(original, Decompress(await SerializeAsync(brotli, async: true), "br")); + + var zstd = new ZstandardCompressedContent(new ByteArrayContent(original), new ZstandardCompressionOptions { Quality = 5 }); + Assert.Equal(original, Decompress(await SerializeAsync(zstd, async: true), "zstd")); + } + } + + [Theory] + [MemberData(nameof(Encodings))] + public async Task SerializeToStream_RepeatableInnerContent_CanSerializeMultipleTimes(string encoding) + { + byte[] original = Encoding.UTF8.GetBytes("The quick brown fox jumps over the lazy dog. " + new string('a', 512)); + HttpContent content = CreateContent(encoding, new ByteArrayContent(original)); + + byte[] first = await SerializeAsync(content, async: true); + byte[] second = await SerializeAsync(content, async: false); + + Assert.Equal(original, Decompress(first, encoding)); + Assert.Equal(original, Decompress(second, encoding)); + } + + [Fact] + public void SerializeToStream_NonRepeatableInnerContent_SecondCallThrows() + { + var inner = new StreamContent(new NonSeekableStream(Encoding.UTF8.GetBytes("data"))); + var content = new GZipCompressedContent(inner); + + using var ms = new MemoryStream(); + content.CopyTo(ms, null, default); + + Assert.Throws(() => content.CopyTo(ms, null, default)); + } + + public static IEnumerable RoundTripData() + { + foreach (bool async in new[] { true, false }) + { + yield return new object[] { "gzip", async }; + + // BrotliStream and ZstandardStream are not supported on browser/wasi. + if (!OperatingSystem.IsBrowser() && !OperatingSystem.IsWasi()) + { + yield return new object[] { "br", async }; + yield return new object[] { "zstd", async }; + } + } + } + + public static IEnumerable Encodings() + { + yield return new object[] { "gzip" }; + + // BrotliStream and ZstandardStream are not supported on browser/wasi. + if (!OperatingSystem.IsBrowser() && !OperatingSystem.IsWasi()) + { + yield return new object[] { "br" }; + yield return new object[] { "zstd" }; + } + } + + #region Helpers + + private static HttpContent CreateContent(string encoding, HttpContent inner) => encoding switch + { + "gzip" => new GZipCompressedContent(inner), + "br" => new BrotliCompressedContent(inner), + "zstd" => new ZstandardCompressedContent(inner), + _ => throw new ArgumentOutOfRangeException(nameof(encoding)) + }; + + private static async Task SerializeAsync(HttpContent content, bool async) + { + using var ms = new MemoryStream(); + + if (async) + { + await content.CopyToAsync(ms); + } + else + { + content.CopyTo(ms, null, default); + } + + return ms.ToArray(); + } + + private static byte[] Decompress(byte[] compressed, string encoding) + { + using var source = new MemoryStream(compressed); + using Stream decompressor = encoding switch + { + "gzip" => new GZipStream(source, CompressionMode.Decompress), + "br" => new BrotliStream(source, CompressionMode.Decompress), + "zstd" => new ZstandardStream(source, CompressionMode.Decompress), + _ => throw new ArgumentOutOfRangeException(nameof(encoding)) + }; + + using var result = new MemoryStream(); + decompressor.CopyTo(result); + return result.ToArray(); + } + + private sealed class NonSeekableStream : MemoryStream + { + public NonSeekableStream(byte[] data) : base(data) { } + + public override bool CanSeek => false; + } + + private sealed class MockContent : HttpContent + { + public int DisposeCount { get; private set; } + + protected override void Dispose(bool disposing) + { + DisposeCount++; + base.Dispose(disposing); + } + + protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) => Task.CompletedTask; + + protected override bool TryComputeLength(out long length) + { + length = 0; + return false; + } + } + + #endregion Helpers + } +} diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj b/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj index b699cc0dafd7fd..4a4c7e893ea7fa 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj @@ -145,6 +145,7 @@ Link="Common\System\Threading\Tasks\TaskTimeoutExtensions.cs" /> + Date: Tue, 7 Jul 2026 22:57:13 +0200 Subject: [PATCH 09/21] Add constructors with CompressionLevel --- .../System.Net.Http/ref/System.Net.Http.cs | 3 +++ .../Net/Http/BrotliCompressedContent.cs | 18 +++++++++++++++++- .../System/Net/Http/GZipCompressedContent.cs | 18 +++++++++++++++++- .../Net/Http/ZstandardCompressedContent.cs | 18 +++++++++++++++++- .../FunctionalTests/CompressedContentTest.cs | 19 +++++++++++++++++++ 5 files changed, 73 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Net.Http/ref/System.Net.Http.cs b/src/libraries/System.Net.Http/ref/System.Net.Http.cs index 72e5834f0b4d69..16795ed5ee826b 100644 --- a/src/libraries/System.Net.Http/ref/System.Net.Http.cs +++ b/src/libraries/System.Net.Http/ref/System.Net.Http.cs @@ -23,6 +23,7 @@ public sealed partial class BrotliCompressedContent : System.Net.Http.HttpConten { public BrotliCompressedContent(System.Net.Http.HttpContent content) { } public BrotliCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.BrotliCompressionOptions compressionOptions) { } + public BrotliCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.CompressionLevel compressionLevel = System.IO.Compression.CompressionLevel.Optimal) { } protected override void Dispose(bool disposing) { } protected override void SerializeToStream(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { } protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context) { throw null; } @@ -33,6 +34,7 @@ public sealed partial class GZipCompressedContent : System.Net.Http.HttpContent { public GZipCompressedContent(System.Net.Http.HttpContent content) { } public GZipCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.ZLibCompressionOptions compressionOptions) { } + public GZipCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.CompressionLevel compressionLevel = System.IO.Compression.CompressionLevel.Optimal) { } protected override void Dispose(bool disposing) { } protected override void SerializeToStream(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { } protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context) { throw null; } @@ -45,6 +47,7 @@ public sealed partial class ZstandardCompressedContent : System.Net.Http.HttpCon { public ZstandardCompressedContent(System.Net.Http.HttpContent content) { } public ZstandardCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.ZstandardCompressionOptions compressionOptions) { } + public ZstandardCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.CompressionLevel compressionLevel = System.IO.Compression.CompressionLevel.Optimal) { } protected override void Dispose(bool disposing) { } protected override void SerializeToStream(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { } protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context) { throw null; } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/BrotliCompressedContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/BrotliCompressedContent.cs index 16b43387385100..2c5426a4e5a21c 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/BrotliCompressedContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/BrotliCompressedContent.cs @@ -20,6 +20,7 @@ public sealed class BrotliCompressedContent : HttpContent private readonly CompressedContentCore _core; private readonly BrotliCompressionOptions? _compressionOptions; + private readonly CompressionLevel _compressionLevel; /// /// Initializes a new instance of the class that compresses the @@ -34,6 +35,21 @@ public BrotliCompressedContent(HttpContent content) CompressedContentCore.InitializeHeaders(this, content, Encoding); } + /// + /// Initializes a new instance of the class that compresses the + /// provided content using the Brotli content coding at the specified compression level. + /// + /// The HTTP content to compress. + /// One of the enumeration values that indicates whether to emphasize speed or compression efficiency. + public BrotliCompressedContent(HttpContent content, CompressionLevel compressionLevel = CompressionLevel.Optimal) + { + ArgumentNullException.ThrowIfNull(content); + + _compressionLevel = compressionLevel; + _core = new CompressedContentCore(content, CreateCompressionStream); + CompressedContentCore.InitializeHeaders(this, content, Encoding); + } + /// /// Initializes a new instance of the class that compresses the /// provided content using the Brotli content coding and the specified compression options. @@ -86,7 +102,7 @@ private BrotliStream CreateCompressionStream(Stream outputStream) } return _compressionOptions is null - ? new BrotliStream(outputStream, CompressionLevel.Optimal, leaveOpen: true) + ? new BrotliStream(outputStream, _compressionLevel, leaveOpen: true) : new BrotliStream(outputStream, _compressionOptions, leaveOpen: true); } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/GZipCompressedContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/GZipCompressedContent.cs index a3b97e83200c41..39ee9f5e72a957 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/GZipCompressedContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/GZipCompressedContent.cs @@ -17,6 +17,7 @@ public sealed class GZipCompressedContent : HttpContent private readonly CompressedContentCore _core; private readonly ZLibCompressionOptions? _compressionOptions; + private readonly CompressionLevel _compressionLevel; /// /// Initializes a new instance of the class that compresses the @@ -31,6 +32,21 @@ public GZipCompressedContent(HttpContent content) CompressedContentCore.InitializeHeaders(this, content, Encoding); } + /// + /// Initializes a new instance of the class that compresses the + /// provided content using the gzip content coding at the specified compression level. + /// + /// The HTTP content to compress. + /// One of the enumeration values that indicates whether to emphasize speed or compression efficiency. + public GZipCompressedContent(HttpContent content, CompressionLevel compressionLevel = CompressionLevel.Optimal) + { + ArgumentNullException.ThrowIfNull(content); + + _compressionLevel = compressionLevel; + _core = new CompressedContentCore(content, CreateCompressionStream); + CompressedContentCore.InitializeHeaders(this, content, Encoding); + } + /// /// Initializes a new instance of the class that compresses the /// provided content using the gzip content coding and the specified compression options. @@ -77,7 +93,7 @@ protected override void Dispose(bool disposing) private GZipStream CreateCompressionStream(Stream outputStream) => _compressionOptions is null - ? new GZipStream(outputStream, CompressionLevel.Optimal, leaveOpen: true) + ? new GZipStream(outputStream, _compressionLevel, leaveOpen: true) : new GZipStream(outputStream, _compressionOptions, leaveOpen: true); } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/ZstandardCompressedContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/ZstandardCompressedContent.cs index 68b78d29bc3225..e58b061ae0a8b2 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/ZstandardCompressedContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/ZstandardCompressedContent.cs @@ -20,6 +20,7 @@ public sealed class ZstandardCompressedContent : HttpContent private readonly CompressedContentCore _core; private readonly ZstandardCompressionOptions? _compressionOptions; + private readonly CompressionLevel _compressionLevel; /// /// Initializes a new instance of the class that compresses the @@ -34,6 +35,21 @@ public ZstandardCompressedContent(HttpContent content) CompressedContentCore.InitializeHeaders(this, content, Encoding); } + /// + /// Initializes a new instance of the class that compresses the + /// provided content using the Zstandard content coding at the specified compression level. + /// + /// The HTTP content to compress. + /// One of the enumeration values that indicates whether to emphasize speed or compression efficiency. + public ZstandardCompressedContent(HttpContent content, CompressionLevel compressionLevel = CompressionLevel.Optimal) + { + ArgumentNullException.ThrowIfNull(content); + + _compressionLevel = compressionLevel; + _core = new CompressedContentCore(content, CreateCompressionStream); + CompressedContentCore.InitializeHeaders(this, content, Encoding); + } + /// /// Initializes a new instance of the class that compresses the /// provided content using the Zstandard content coding and the specified compression options. @@ -86,7 +102,7 @@ private ZstandardStream CreateCompressionStream(Stream outputStream) } return _compressionOptions is null - ? new ZstandardStream(outputStream, CompressionLevel.Optimal, leaveOpen: true) + ? new ZstandardStream(outputStream, _compressionLevel, leaveOpen: true) : new ZstandardStream(outputStream, _compressionOptions, leaveOpen: true); } } diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.cs index 342751d26a9aff..0e41d8ece41371 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.cs @@ -20,10 +20,13 @@ public void Ctor_NullContent_ThrowsArgumentNullException() { AssertExtensions.Throws("content", () => new GZipCompressedContent(null)); AssertExtensions.Throws("content", () => new GZipCompressedContent(null, new ZLibCompressionOptions())); + AssertExtensions.Throws("content", () => new GZipCompressedContent(null, CompressionLevel.SmallestSize)); AssertExtensions.Throws("content", () => new BrotliCompressedContent(null)); AssertExtensions.Throws("content", () => new BrotliCompressedContent(null, new BrotliCompressionOptions())); + AssertExtensions.Throws("content", () => new BrotliCompressedContent(null, CompressionLevel.SmallestSize)); AssertExtensions.Throws("content", () => new ZstandardCompressedContent(null)); AssertExtensions.Throws("content", () => new ZstandardCompressedContent(null, new ZstandardCompressionOptions())); + AssertExtensions.Throws("content", () => new ZstandardCompressedContent(null, CompressionLevel.SmallestSize)); } [Fact] @@ -119,6 +122,22 @@ public async Task SerializeToStream_WithOptions_RoundTrips() } } + [Theory] + [MemberData(nameof(Encodings))] + public async Task SerializeToStream_WithCompressionLevel_RoundTrips(string encoding) + { + byte[] original = Encoding.UTF8.GetBytes(new string('a', 4096)); + HttpContent content = encoding switch + { + "gzip" => new GZipCompressedContent(new ByteArrayContent(original), CompressionLevel.SmallestSize), + "br" => new BrotliCompressedContent(new ByteArrayContent(original), CompressionLevel.SmallestSize), + "zstd" => new ZstandardCompressedContent(new ByteArrayContent(original), CompressionLevel.SmallestSize), + _ => throw new ArgumentOutOfRangeException(nameof(encoding)) + }; + + Assert.Equal(original, Decompress(await SerializeAsync(content, async: true), encoding)); + } + [Theory] [MemberData(nameof(Encodings))] public async Task SerializeToStream_RepeatableInnerContent_CanSerializeMultipleTimes(string encoding) From 268e1e62fc0b6c3843cab94853f39ff2d901141b Mon Sep 17 00:00:00 2001 From: iremyux Date: Tue, 7 Jul 2026 23:22:15 +0200 Subject: [PATCH 10/21] Propagate context to the inner HttpContent in SerializeToStreamAsync --- .../src/System/Net/Http/BrotliCompressedContent.cs | 4 ++-- .../src/System/Net/Http/CompressedContentCore.cs | 4 ++-- .../src/System/Net/Http/GZipCompressedContent.cs | 4 ++-- .../src/System/Net/Http/ZstandardCompressedContent.cs | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/BrotliCompressedContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/BrotliCompressedContent.cs index 2c5426a4e5a21c..3deb352e5eea24 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/BrotliCompressedContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/BrotliCompressedContent.cs @@ -70,10 +70,10 @@ protected override void SerializeToStream(Stream stream, TransportContext? conte _core.SerializeToStream(stream, context, cancellationToken); protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) => - _core.SerializeToStreamAsync(stream, CancellationToken.None); + _core.SerializeToStreamAsync(stream, context, CancellationToken.None); protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) => - _core.SerializeToStreamAsync(stream, cancellationToken); + _core.SerializeToStreamAsync(stream, context, cancellationToken); protected internal override bool TryComputeLength(out long length) { diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContentCore.cs b/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContentCore.cs index fb1c55973a8cd5..a579d67046f169 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContentCore.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContentCore.cs @@ -50,12 +50,12 @@ public void SerializeToStream(Stream stream, TransportContext? context, Cancella _originalContent.CopyTo(compressionStream, context, cancellationToken); } - public async Task SerializeToStreamAsync(Stream stream, CancellationToken cancellationToken) + public async Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) { Stream compressionStream = _createCompressionStream(stream); await using (compressionStream.ConfigureAwait(false)) { - await _originalContent.CopyToAsync(compressionStream, cancellationToken).ConfigureAwait(false); + await _originalContent.CopyToAsync(compressionStream, context, cancellationToken).ConfigureAwait(false); } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/GZipCompressedContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/GZipCompressedContent.cs index 39ee9f5e72a957..e2614de5acd7db 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/GZipCompressedContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/GZipCompressedContent.cs @@ -67,10 +67,10 @@ protected override void SerializeToStream(Stream stream, TransportContext? conte _core.SerializeToStream(stream, context, cancellationToken); protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) => - _core.SerializeToStreamAsync(stream, CancellationToken.None); + _core.SerializeToStreamAsync(stream, context, CancellationToken.None); protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) => - _core.SerializeToStreamAsync(stream, cancellationToken); + _core.SerializeToStreamAsync(stream, context, cancellationToken); protected internal override bool TryComputeLength(out long length) { diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/ZstandardCompressedContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/ZstandardCompressedContent.cs index e58b061ae0a8b2..26ef3283083fd8 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/ZstandardCompressedContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/ZstandardCompressedContent.cs @@ -70,10 +70,10 @@ protected override void SerializeToStream(Stream stream, TransportContext? conte _core.SerializeToStream(stream, context, cancellationToken); protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) => - _core.SerializeToStreamAsync(stream, CancellationToken.None); + _core.SerializeToStreamAsync(stream, context, CancellationToken.None); protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) => - _core.SerializeToStreamAsync(stream, cancellationToken); + _core.SerializeToStreamAsync(stream, context, cancellationToken); protected internal override bool TryComputeLength(out long length) { From fb074ac7fa9edff834501175eb3e391d731fb3f1 Mon Sep 17 00:00:00 2001 From: Irem Yuksel <113098562+iremyux@users.noreply.github.com> Date: Thu, 9 Jul 2026 12:34:53 +0200 Subject: [PATCH 11/21] Remove endregion Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../tests/FunctionalTests/CompressedContentTest.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.cs index 0e41d8ece41371..32731c254c8dac 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.cs @@ -259,6 +259,5 @@ protected override bool TryComputeLength(out long length) } } - #endregion Helpers } } From cc09490e1a0ad1a0db402195b3d31a05c7ff5649 Mon Sep 17 00:00:00 2001 From: iremyux Date: Thu, 9 Jul 2026 12:37:01 +0200 Subject: [PATCH 12/21] Revert "Remove endregion" This reverts commit fb074ac7fa9edff834501175eb3e391d731fb3f1. --- .../tests/FunctionalTests/CompressedContentTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.cs index 32731c254c8dac..0e41d8ece41371 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.cs @@ -259,5 +259,6 @@ protected override bool TryComputeLength(out long length) } } + #endregion Helpers } } From 97bc66ad0777a47a013490416b8e6dbd44423f95 Mon Sep 17 00:00:00 2001 From: iremyux Date: Thu, 9 Jul 2026 12:44:08 +0200 Subject: [PATCH 13/21] Remove platform support precheck --- .../src/System/Net/Http/BrotliCompressedContent.cs | 11 ++--------- .../src/System/Net/Http/ZstandardCompressedContent.cs | 11 ++--------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/BrotliCompressedContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/BrotliCompressedContent.cs index 3deb352e5eea24..3336fb3508a914 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/BrotliCompressedContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/BrotliCompressedContent.cs @@ -94,16 +94,9 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } - private BrotliStream CreateCompressionStream(Stream outputStream) - { - if (OperatingSystem.IsBrowser() || OperatingSystem.IsWasi()) - { - throw new PlatformNotSupportedException(); - } - - return _compressionOptions is null + private BrotliStream CreateCompressionStream(Stream outputStream) => + _compressionOptions is null ? new BrotliStream(outputStream, _compressionLevel, leaveOpen: true) : new BrotliStream(outputStream, _compressionOptions, leaveOpen: true); - } } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/ZstandardCompressedContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/ZstandardCompressedContent.cs index 26ef3283083fd8..812f51b559b2d6 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/ZstandardCompressedContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/ZstandardCompressedContent.cs @@ -94,16 +94,9 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } - private ZstandardStream CreateCompressionStream(Stream outputStream) - { - if (OperatingSystem.IsBrowser() || OperatingSystem.IsWasi()) - { - throw new PlatformNotSupportedException(); - } - - return _compressionOptions is null + private ZstandardStream CreateCompressionStream(Stream outputStream) => + _compressionOptions is null ? new ZstandardStream(outputStream, _compressionLevel, leaveOpen: true) : new ZstandardStream(outputStream, _compressionOptions, leaveOpen: true); - } } } From 4c41953b00c92473b8595c1171cab6738264e3e9 Mon Sep 17 00:00:00 2001 From: iremyux Date: Thu, 9 Jul 2026 12:44:19 +0200 Subject: [PATCH 14/21] Remove regions --- .../tests/FunctionalTests/CompressedContentTest.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.cs index 0e41d8ece41371..a7f5ea09a4c645 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.cs @@ -191,8 +191,6 @@ public static IEnumerable Encodings() } } - #region Helpers - private static HttpContent CreateContent(string encoding, HttpContent inner) => encoding switch { "gzip" => new GZipCompressedContent(inner), @@ -258,7 +256,5 @@ protected override bool TryComputeLength(out long length) return false; } } - - #endregion Helpers } } From c2512a349a101f2b24b8fd072a6f3e9ac26edfab Mon Sep 17 00:00:00 2001 From: iremyux Date: Thu, 9 Jul 2026 13:06:20 +0200 Subject: [PATCH 15/21] Remove the standalone ctor --- .../System.Net.Http/ref/System.Net.Http.cs | 3 --- .../src/System/Net/Http/BrotliCompressedContent.cs | 13 ------------- .../src/System/Net/Http/GZipCompressedContent.cs | 13 ------------- .../System/Net/Http/ZstandardCompressedContent.cs | 13 ------------- .../tests/FunctionalTests/CompressedContentTest.cs | 2 +- 5 files changed, 1 insertion(+), 43 deletions(-) diff --git a/src/libraries/System.Net.Http/ref/System.Net.Http.cs b/src/libraries/System.Net.Http/ref/System.Net.Http.cs index 16795ed5ee826b..10bacd9ee63866 100644 --- a/src/libraries/System.Net.Http/ref/System.Net.Http.cs +++ b/src/libraries/System.Net.Http/ref/System.Net.Http.cs @@ -21,7 +21,6 @@ protected override void SerializeToStream(System.IO.Stream stream, System.Net.Tr [System.Runtime.Versioning.UnsupportedOSPlatform("wasi")] public sealed partial class BrotliCompressedContent : System.Net.Http.HttpContent { - public BrotliCompressedContent(System.Net.Http.HttpContent content) { } public BrotliCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.BrotliCompressionOptions compressionOptions) { } public BrotliCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.CompressionLevel compressionLevel = System.IO.Compression.CompressionLevel.Optimal) { } protected override void Dispose(bool disposing) { } @@ -32,7 +31,6 @@ protected override void SerializeToStream(System.IO.Stream stream, System.Net.Tr } public sealed partial class GZipCompressedContent : System.Net.Http.HttpContent { - public GZipCompressedContent(System.Net.Http.HttpContent content) { } public GZipCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.ZLibCompressionOptions compressionOptions) { } public GZipCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.CompressionLevel compressionLevel = System.IO.Compression.CompressionLevel.Optimal) { } protected override void Dispose(bool disposing) { } @@ -45,7 +43,6 @@ protected override void SerializeToStream(System.IO.Stream stream, System.Net.Tr [System.Runtime.Versioning.UnsupportedOSPlatform("wasi")] public sealed partial class ZstandardCompressedContent : System.Net.Http.HttpContent { - public ZstandardCompressedContent(System.Net.Http.HttpContent content) { } public ZstandardCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.ZstandardCompressionOptions compressionOptions) { } public ZstandardCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.CompressionLevel compressionLevel = System.IO.Compression.CompressionLevel.Optimal) { } protected override void Dispose(bool disposing) { } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/BrotliCompressedContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/BrotliCompressedContent.cs index 3336fb3508a914..b05528887cb1ab 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/BrotliCompressedContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/BrotliCompressedContent.cs @@ -22,19 +22,6 @@ public sealed class BrotliCompressedContent : HttpContent private readonly BrotliCompressionOptions? _compressionOptions; private readonly CompressionLevel _compressionLevel; - /// - /// Initializes a new instance of the class that compresses the - /// provided content using the Brotli content coding with default compression settings. - /// - /// The HTTP content to compress. - public BrotliCompressedContent(HttpContent content) - { - ArgumentNullException.ThrowIfNull(content); - - _core = new CompressedContentCore(content, CreateCompressionStream); - CompressedContentCore.InitializeHeaders(this, content, Encoding); - } - /// /// Initializes a new instance of the class that compresses the /// provided content using the Brotli content coding at the specified compression level. diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/GZipCompressedContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/GZipCompressedContent.cs index e2614de5acd7db..093bd7ff946914 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/GZipCompressedContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/GZipCompressedContent.cs @@ -19,19 +19,6 @@ public sealed class GZipCompressedContent : HttpContent private readonly ZLibCompressionOptions? _compressionOptions; private readonly CompressionLevel _compressionLevel; - /// - /// Initializes a new instance of the class that compresses the - /// provided content using the gzip content coding with default compression settings. - /// - /// The HTTP content to compress. - public GZipCompressedContent(HttpContent content) - { - ArgumentNullException.ThrowIfNull(content); - - _core = new CompressedContentCore(content, CreateCompressionStream); - CompressedContentCore.InitializeHeaders(this, content, Encoding); - } - /// /// Initializes a new instance of the class that compresses the /// provided content using the gzip content coding at the specified compression level. diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/ZstandardCompressedContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/ZstandardCompressedContent.cs index 812f51b559b2d6..ae7b115bea4574 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/ZstandardCompressedContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/ZstandardCompressedContent.cs @@ -22,19 +22,6 @@ public sealed class ZstandardCompressedContent : HttpContent private readonly ZstandardCompressionOptions? _compressionOptions; private readonly CompressionLevel _compressionLevel; - /// - /// Initializes a new instance of the class that compresses the - /// provided content using the Zstandard content coding with default compression settings. - /// - /// The HTTP content to compress. - public ZstandardCompressedContent(HttpContent content) - { - ArgumentNullException.ThrowIfNull(content); - - _core = new CompressedContentCore(content, CreateCompressionStream); - CompressedContentCore.InitializeHeaders(this, content, Encoding); - } - /// /// Initializes a new instance of the class that compresses the /// provided content using the Zstandard content coding at the specified compression level. diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.cs index a7f5ea09a4c645..2cc0b2032013b7 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.cs @@ -248,7 +248,7 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } - protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) => Task.CompletedTask; + protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) => Task.CompletedTask; protected override bool TryComputeLength(out long length) { From d6d2109ea44edbce4079907af5550602540f8fe9 Mon Sep 17 00:00:00 2001 From: iremyux Date: Thu, 9 Jul 2026 14:10:37 +0200 Subject: [PATCH 16/21] Alphabetical order on ref file --- .../System.Net.Http/ref/System.Net.Http.cs | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/libraries/System.Net.Http/ref/System.Net.Http.cs b/src/libraries/System.Net.Http/ref/System.Net.Http.cs index 10bacd9ee63866..d4902ac5506585 100644 --- a/src/libraries/System.Net.Http/ref/System.Net.Http.cs +++ b/src/libraries/System.Net.Http/ref/System.Net.Http.cs @@ -6,17 +6,6 @@ namespace System.Net.Http { - public partial class ByteArrayContent : System.Net.Http.HttpContent - { - public ByteArrayContent(byte[] content) { } - public ByteArrayContent(byte[] content, int offset, int count) { } - protected override System.IO.Stream CreateContentReadStream(System.Threading.CancellationToken cancellationToken) { throw null; } - protected override System.Threading.Tasks.Task CreateContentReadStreamAsync() { throw null; } - protected override void SerializeToStream(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { } - protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context) { throw null; } - protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { throw null; } - protected internal override bool TryComputeLength(out long length) { throw null; } - } [System.Runtime.Versioning.UnsupportedOSPlatform("browser")] [System.Runtime.Versioning.UnsupportedOSPlatform("wasi")] public sealed partial class BrotliCompressedContent : System.Net.Http.HttpContent @@ -29,23 +18,12 @@ protected override void SerializeToStream(System.IO.Stream stream, System.Net.Tr protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { throw null; } protected internal override bool TryComputeLength(out long length) { throw null; } } - public sealed partial class GZipCompressedContent : System.Net.Http.HttpContent - { - public GZipCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.ZLibCompressionOptions compressionOptions) { } - public GZipCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.CompressionLevel compressionLevel = System.IO.Compression.CompressionLevel.Optimal) { } - protected override void Dispose(bool disposing) { } - protected override void SerializeToStream(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { } - protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context) { throw null; } - protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { throw null; } - protected internal override bool TryComputeLength(out long length) { throw null; } - } - [System.Runtime.Versioning.UnsupportedOSPlatform("browser")] - [System.Runtime.Versioning.UnsupportedOSPlatform("wasi")] - public sealed partial class ZstandardCompressedContent : System.Net.Http.HttpContent + public partial class ByteArrayContent : System.Net.Http.HttpContent { - public ZstandardCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.ZstandardCompressionOptions compressionOptions) { } - public ZstandardCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.CompressionLevel compressionLevel = System.IO.Compression.CompressionLevel.Optimal) { } - protected override void Dispose(bool disposing) { } + public ByteArrayContent(byte[] content) { } + public ByteArrayContent(byte[] content, int offset, int count) { } + protected override System.IO.Stream CreateContentReadStream(System.Threading.CancellationToken cancellationToken) { throw null; } + protected override System.Threading.Tasks.Task CreateContentReadStreamAsync() { throw null; } protected override void SerializeToStream(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { } protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context) { throw null; } protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { throw null; } @@ -76,6 +54,16 @@ public FormUrlEncodedContent( >> nameValueCollection) : base (default(byte[])) { } protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { throw null; } } + public sealed partial class GZipCompressedContent : System.Net.Http.HttpContent + { + public GZipCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.ZLibCompressionOptions compressionOptions) { } + public GZipCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.CompressionLevel compressionLevel = System.IO.Compression.CompressionLevel.Optimal) { } + protected override void Dispose(bool disposing) { } + protected override void SerializeToStream(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { } + protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context) { throw null; } + protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { throw null; } + protected internal override bool TryComputeLength(out long length) { throw null; } + } public delegate System.Text.Encoding? HeaderEncodingSelector(string headerName, TContext context); public partial class HttpClient : System.Net.Http.HttpMessageInvoker { @@ -535,6 +523,18 @@ public StringContent(string content, System.Text.Encoding? encoding, System.Net. public StringContent(string content, System.Text.Encoding? encoding, string? mediaType) : base (default(byte[])) { } protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { throw null; } } + [System.Runtime.Versioning.UnsupportedOSPlatform("browser")] + [System.Runtime.Versioning.UnsupportedOSPlatform("wasi")] + public sealed partial class ZstandardCompressedContent : System.Net.Http.HttpContent + { + public ZstandardCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.ZstandardCompressionOptions compressionOptions) { } + public ZstandardCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.CompressionLevel compressionLevel = System.IO.Compression.CompressionLevel.Optimal) { } + protected override void Dispose(bool disposing) { } + protected override void SerializeToStream(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { } + protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context) { throw null; } + protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { throw null; } + protected internal override bool TryComputeLength(out long length) { throw null; } + } } namespace System.Net.Http.Headers { From c4e7998850cb57f3cf3c88ff09c98e89493cdf56 Mon Sep 17 00:00:00 2001 From: iremyux Date: Mon, 13 Jul 2026 16:38:56 +0200 Subject: [PATCH 17/21] Generated ref file changes --- src/libraries/System.Net.Http/ref/System.Net.Http.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libraries/System.Net.Http/ref/System.Net.Http.cs b/src/libraries/System.Net.Http/ref/System.Net.Http.cs index d4902ac5506585..2d636ea25820f0 100644 --- a/src/libraries/System.Net.Http/ref/System.Net.Http.cs +++ b/src/libraries/System.Net.Http/ref/System.Net.Http.cs @@ -6,8 +6,8 @@ namespace System.Net.Http { - [System.Runtime.Versioning.UnsupportedOSPlatform("browser")] - [System.Runtime.Versioning.UnsupportedOSPlatform("wasi")] + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")] + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("wasi")] public sealed partial class BrotliCompressedContent : System.Net.Http.HttpContent { public BrotliCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.BrotliCompressionOptions compressionOptions) { } @@ -56,8 +56,8 @@ public FormUrlEncodedContent( } public sealed partial class GZipCompressedContent : System.Net.Http.HttpContent { - public GZipCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.ZLibCompressionOptions compressionOptions) { } public GZipCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.CompressionLevel compressionLevel = System.IO.Compression.CompressionLevel.Optimal) { } + public GZipCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.ZLibCompressionOptions compressionOptions) { } protected override void Dispose(bool disposing) { } protected override void SerializeToStream(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { } protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context) { throw null; } @@ -523,12 +523,12 @@ public StringContent(string content, System.Text.Encoding? encoding, System.Net. public StringContent(string content, System.Text.Encoding? encoding, string? mediaType) : base (default(byte[])) { } protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { throw null; } } - [System.Runtime.Versioning.UnsupportedOSPlatform("browser")] - [System.Runtime.Versioning.UnsupportedOSPlatform("wasi")] + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")] + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("wasi")] public sealed partial class ZstandardCompressedContent : System.Net.Http.HttpContent { - public ZstandardCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.ZstandardCompressionOptions compressionOptions) { } public ZstandardCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.CompressionLevel compressionLevel = System.IO.Compression.CompressionLevel.Optimal) { } + public ZstandardCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.ZstandardCompressionOptions compressionOptions) { } protected override void Dispose(bool disposing) { } protected override void SerializeToStream(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { } protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context) { throw null; } From d3bda4e82bc84e47a60287680c29be5c653f92b2 Mon Sep 17 00:00:00 2001 From: iremyux Date: Tue, 14 Jul 2026 14:09:30 +0200 Subject: [PATCH 18/21] Address copilot code reviews --- .../src/System.Net.Http.csproj | 6 +- .../Net/Http/BrotliCompressedContent.cs | 1 + .../System/Net/Http/CompressedContentCore.cs | 15 +- .../System/Net/Http/GZipCompressedContent.cs | 1 + .../Net/Http/ZstandardCompressedContent.cs | 1 + .../CompressedContentTest.NonBrowser.cs | 157 +++++++++++++++++ .../FunctionalTests/CompressedContentTest.cs | 159 +++++++----------- .../System.Net.Http.Functional.Tests.csproj | 3 +- 8 files changed, 235 insertions(+), 108 deletions(-) create mode 100644 src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.NonBrowser.cs diff --git a/src/libraries/System.Net.Http/src/System.Net.Http.csproj b/src/libraries/System.Net.Http/src/System.Net.Http.csproj index 04a0e2172b27fd..b09f329d938ab3 100644 --- a/src/libraries/System.Net.Http/src/System.Net.Http.csproj +++ b/src/libraries/System.Net.Http/src/System.Net.Http.csproj @@ -23,13 +23,11 @@ + - - - @@ -38,6 +36,7 @@ + @@ -79,6 +78,7 @@ + diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/BrotliCompressedContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/BrotliCompressedContent.cs index b05528887cb1ab..7604077543aec1 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/BrotliCompressedContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/BrotliCompressedContent.cs @@ -31,6 +31,7 @@ public sealed class BrotliCompressedContent : HttpContent public BrotliCompressedContent(HttpContent content, CompressionLevel compressionLevel = CompressionLevel.Optimal) { ArgumentNullException.ThrowIfNull(content); + CompressedContentCore.ValidateCompressionLevel(compressionLevel, nameof(compressionLevel)); _compressionLevel = compressionLevel; _core = new CompressedContentCore(content, CreateCompressionStream); diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContentCore.cs b/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContentCore.cs index a579d67046f169..792f0d1d0a541c 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContentCore.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContentCore.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.IO; +using System.IO.Compression; using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; @@ -23,14 +24,24 @@ public CompressedContentCore(HttpContent originalContent, Func c _createCompressionStream = createCompressionStream; } + public static void ValidateCompressionLevel(CompressionLevel compressionLevel, string paramName) + { + // Validate up front so an invalid enum value fails fast at construction time rather than + // deferring the exception to the serialization path when the request is being sent. + if (compressionLevel is < CompressionLevel.Optimal or > CompressionLevel.SmallestSize) + { + throw new ArgumentOutOfRangeException(paramName); + } + } + public static void InitializeHeaders(HttpContent target, HttpContent source, string encoding) { // Copy headers from the original content. target.Headers.AddHeaders(source.Headers); // Append our encoding to the Content-Encoding header (supports stacking per HTTP spec). - // The common case is that the inner content has no Content-Encoding, so take the - // allocation-free fast path and only materialize the collection when we need to stack. + // The 99% case is that the inner content has no Content-Encoding, so take the fast path + // that allocates less and only materialize the collection when we actually need to stack. if (target.Headers.Contains(KnownHeaders.ContentEncoding.Descriptor)) { target.Headers.ContentEncoding.Add(encoding); diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/GZipCompressedContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/GZipCompressedContent.cs index 093bd7ff946914..ff6342f1ef7e19 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/GZipCompressedContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/GZipCompressedContent.cs @@ -28,6 +28,7 @@ public sealed class GZipCompressedContent : HttpContent public GZipCompressedContent(HttpContent content, CompressionLevel compressionLevel = CompressionLevel.Optimal) { ArgumentNullException.ThrowIfNull(content); + CompressedContentCore.ValidateCompressionLevel(compressionLevel, nameof(compressionLevel)); _compressionLevel = compressionLevel; _core = new CompressedContentCore(content, CreateCompressionStream); diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/ZstandardCompressedContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/ZstandardCompressedContent.cs index ae7b115bea4574..460b1147b13249 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/ZstandardCompressedContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/ZstandardCompressedContent.cs @@ -31,6 +31,7 @@ public sealed class ZstandardCompressedContent : HttpContent public ZstandardCompressedContent(HttpContent content, CompressionLevel compressionLevel = CompressionLevel.Optimal) { ArgumentNullException.ThrowIfNull(content); + CompressedContentCore.ValidateCompressionLevel(compressionLevel, nameof(compressionLevel)); _compressionLevel = compressionLevel; _core = new CompressedContentCore(content, CreateCompressionStream); diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.NonBrowser.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.NonBrowser.cs new file mode 100644 index 00000000000000..c2ac84bfa99f69 --- /dev/null +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.NonBrowser.cs @@ -0,0 +1,157 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.IO.Compression; +using System.Text; +using System.Threading.Tasks; + +using Xunit; + +namespace System.Net.Http.Functional.Tests +{ + // Brotli and Zstandard rely on native libraries that are not available on browser/wasi, so both + // types are annotated [UnsupportedOSPlatform("browser"/"wasi")]. This file is excluded from those + // builds; the gzip tests and shared helpers live in CompressedContentTest.cs. + public partial class CompressedContentTest + { + [Fact] + public void BrotliZstd_Ctor_NullContent_ThrowsArgumentNullException() + { + AssertExtensions.Throws("content", () => new BrotliCompressedContent(null)); + AssertExtensions.Throws("content", () => new BrotliCompressedContent(null, new BrotliCompressionOptions())); + AssertExtensions.Throws("content", () => new BrotliCompressedContent(null, CompressionLevel.SmallestSize)); + AssertExtensions.Throws("content", () => new ZstandardCompressedContent(null)); + AssertExtensions.Throws("content", () => new ZstandardCompressedContent(null, new ZstandardCompressionOptions())); + AssertExtensions.Throws("content", () => new ZstandardCompressedContent(null, CompressionLevel.SmallestSize)); + } + + [Fact] + public void BrotliZstd_Ctor_NullOptions_ThrowsArgumentNullException() + { + var inner = new ByteArrayContent(Array.Empty()); + AssertExtensions.Throws("compressionOptions", () => new BrotliCompressedContent(inner, null)); + AssertExtensions.Throws("compressionOptions", () => new ZstandardCompressedContent(inner, null)); + } + + [Theory] + [InlineData((CompressionLevel)(-1))] + [InlineData((CompressionLevel)4)] + [InlineData((CompressionLevel)99)] + public void BrotliZstd_Ctor_InvalidCompressionLevel_ThrowsArgumentOutOfRangeException(CompressionLevel compressionLevel) + { + var inner = new ByteArrayContent(Array.Empty()); + AssertExtensions.Throws("compressionLevel", () => new BrotliCompressedContent(inner, compressionLevel)); + AssertExtensions.Throws("compressionLevel", () => new ZstandardCompressedContent(inner, compressionLevel)); + } + + [Theory] + [InlineData(CompressionLevel.Optimal)] + [InlineData(CompressionLevel.Fastest)] + [InlineData(CompressionLevel.NoCompression)] + [InlineData(CompressionLevel.SmallestSize)] + public void BrotliZstd_Ctor_ValidCompressionLevel_DoesNotThrow(CompressionLevel compressionLevel) + { + var inner = new ByteArrayContent(Array.Empty()); + _ = new BrotliCompressedContent(inner, compressionLevel); + _ = new ZstandardCompressedContent(inner, compressionLevel); + } + + [Theory] + [InlineData("br")] + [InlineData("zstd")] + public void BrotliZstd_Ctor_SetsContentEncodingHeader(string encoding) + { + HttpContent content = CreateBrotliOrZstdContent(encoding, new ByteArrayContent(Array.Empty())); + + Assert.Equal(new[] { encoding }, content.Headers.ContentEncoding); + } + + [Fact] + public void BrotliZstd_Ctor_NestedContent_StacksContentEncoding() + { + var inner = new GZipCompressedContent(new ByteArrayContent(Array.Empty())); + var outer = new BrotliCompressedContent(inner); + + Assert.Equal(new[] { "gzip", "br" }, outer.Headers.ContentEncoding); + } + + [Theory] + [InlineData("br", true)] + [InlineData("br", false)] + [InlineData("zstd", true)] + [InlineData("zstd", false)] + public async Task BrotliZstd_SerializeToStream_RoundTrips_MatchesOriginal(string encoding, bool async) + { + byte[] original = Encoding.UTF8.GetBytes("The quick brown fox jumps over the lazy dog. " + new string('a', 1024)); + HttpContent content = CreateBrotliOrZstdContent(encoding, new ByteArrayContent(original)); + + byte[] compressed = await SerializeAsync(content, async); + + Assert.Equal(original, DecompressBrotliOrZstd(compressed, encoding)); + } + + [Theory] + [InlineData("br")] + [InlineData("zstd")] + public async Task BrotliZstd_SerializeToStream_WithOptions_RoundTrips(string encoding) + { + byte[] original = Encoding.UTF8.GetBytes(new string('a', 4096)); + HttpContent content = encoding == "br" + ? new BrotliCompressedContent(new ByteArrayContent(original), new BrotliCompressionOptions { Quality = 5 }) + : new ZstandardCompressedContent(new ByteArrayContent(original), new ZstandardCompressionOptions { Quality = 5 }); + + Assert.Equal(original, DecompressBrotliOrZstd(await SerializeAsync(content, async: true), encoding)); + } + + [Theory] + [InlineData("br")] + [InlineData("zstd")] + public async Task BrotliZstd_SerializeToStream_WithCompressionLevel_RoundTrips(string encoding) + { + byte[] original = Encoding.UTF8.GetBytes(new string('a', 4096)); + HttpContent content = encoding == "br" + ? new BrotliCompressedContent(new ByteArrayContent(original), CompressionLevel.SmallestSize) + : new ZstandardCompressedContent(new ByteArrayContent(original), CompressionLevel.SmallestSize); + + Assert.Equal(original, DecompressBrotliOrZstd(await SerializeAsync(content, async: true), encoding)); + } + + [Theory] + [InlineData("br")] + [InlineData("zstd")] + public async Task BrotliZstd_SerializeToStream_RepeatableInnerContent_CanSerializeMultipleTimes(string encoding) + { + byte[] original = Encoding.UTF8.GetBytes("The quick brown fox jumps over the lazy dog. " + new string('a', 512)); + HttpContent content = CreateBrotliOrZstdContent(encoding, new ByteArrayContent(original)); + + byte[] first = await SerializeAsync(content, async: true); + byte[] second = await SerializeAsync(content, async: false); + + Assert.Equal(original, DecompressBrotliOrZstd(first, encoding)); + Assert.Equal(original, DecompressBrotliOrZstd(second, encoding)); + } + + private static HttpContent CreateBrotliOrZstdContent(string encoding, HttpContent inner) => encoding switch + { + "br" => new BrotliCompressedContent(inner), + "zstd" => new ZstandardCompressedContent(inner), + _ => throw new ArgumentOutOfRangeException(nameof(encoding)) + }; + + private static byte[] DecompressBrotliOrZstd(byte[] compressed, string encoding) + { + using var source = new MemoryStream(compressed); + using Stream decompressor = encoding switch + { + "br" => new BrotliStream(source, CompressionMode.Decompress), + "zstd" => new ZstandardStream(source, CompressionMode.Decompress), + _ => throw new ArgumentOutOfRangeException(nameof(encoding)) + }; + + using var result = new MemoryStream(); + decompressor.CopyTo(result); + return result.ToArray(); + } + } +} diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.cs index 2cc0b2032013b7..b829dd216f940a 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.cs @@ -1,56 +1,68 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Net.Http.Headers; using System.Text; -using System.Threading; using System.Threading.Tasks; using Xunit; namespace System.Net.Http.Functional.Tests { - public class CompressedContentTest + // GZip is supported on every platform (including browser/wasi), so the gzip tests and the shared + // helpers live in this file, which is compiled for all targets. Brotli and Zstandard are not + // supported on browser/wasi; their tests live in CompressedContentTest.NonBrowser.cs, which is + // excluded from those builds. + public partial class CompressedContentTest { [Fact] - public void Ctor_NullContent_ThrowsArgumentNullException() + public void GZip_Ctor_NullContent_ThrowsArgumentNullException() { AssertExtensions.Throws("content", () => new GZipCompressedContent(null)); AssertExtensions.Throws("content", () => new GZipCompressedContent(null, new ZLibCompressionOptions())); AssertExtensions.Throws("content", () => new GZipCompressedContent(null, CompressionLevel.SmallestSize)); - AssertExtensions.Throws("content", () => new BrotliCompressedContent(null)); - AssertExtensions.Throws("content", () => new BrotliCompressedContent(null, new BrotliCompressionOptions())); - AssertExtensions.Throws("content", () => new BrotliCompressedContent(null, CompressionLevel.SmallestSize)); - AssertExtensions.Throws("content", () => new ZstandardCompressedContent(null)); - AssertExtensions.Throws("content", () => new ZstandardCompressedContent(null, new ZstandardCompressionOptions())); - AssertExtensions.Throws("content", () => new ZstandardCompressedContent(null, CompressionLevel.SmallestSize)); } [Fact] - public void Ctor_NullOptions_ThrowsArgumentNullException() + public void GZip_Ctor_NullOptions_ThrowsArgumentNullException() { var inner = new ByteArrayContent(Array.Empty()); AssertExtensions.Throws("compressionOptions", () => new GZipCompressedContent(inner, null)); - AssertExtensions.Throws("compressionOptions", () => new BrotliCompressedContent(inner, null)); - AssertExtensions.Throws("compressionOptions", () => new ZstandardCompressedContent(inner, null)); } [Theory] - [InlineData("gzip")] - [InlineData("br")] - [InlineData("zstd")] - public void Ctor_SetsContentEncodingHeader(string encoding) + [InlineData((CompressionLevel)(-1))] + [InlineData((CompressionLevel)4)] + [InlineData((CompressionLevel)99)] + public void GZip_Ctor_InvalidCompressionLevel_ThrowsArgumentOutOfRangeException(CompressionLevel compressionLevel) { - HttpContent content = CreateContent(encoding, new ByteArrayContent(Array.Empty())); + var inner = new ByteArrayContent(Array.Empty()); + AssertExtensions.Throws("compressionLevel", () => new GZipCompressedContent(inner, compressionLevel)); + } - Assert.Equal(new[] { encoding }, content.Headers.ContentEncoding); + [Theory] + [InlineData(CompressionLevel.Optimal)] + [InlineData(CompressionLevel.Fastest)] + [InlineData(CompressionLevel.NoCompression)] + [InlineData(CompressionLevel.SmallestSize)] + public void GZip_Ctor_ValidCompressionLevel_DoesNotThrow(CompressionLevel compressionLevel) + { + var inner = new ByteArrayContent(Array.Empty()); + _ = new GZipCompressedContent(inner, compressionLevel); } [Fact] - public void Ctor_RemovesContentLength() + public void GZip_Ctor_SetsContentEncodingHeader() + { + var content = new GZipCompressedContent(new ByteArrayContent(Array.Empty())); + + Assert.Equal(new[] { "gzip" }, content.Headers.ContentEncoding); + } + + [Fact] + public void GZip_Ctor_RemovesContentLength() { var inner = new ByteArrayContent(new byte[10]); Assert.Equal(10, inner.Headers.ContentLength); @@ -61,7 +73,7 @@ public void Ctor_RemovesContentLength() } [Fact] - public void Ctor_CopiesInnerContentHeaders() + public void GZip_Ctor_CopiesInnerContentHeaders() { var inner = new ByteArrayContent(Array.Empty()); inner.Headers.ContentType = new MediaTypeHeaderValue("application/json"); @@ -72,16 +84,16 @@ public void Ctor_CopiesInnerContentHeaders() } [Fact] - public void Ctor_NestedContent_StacksContentEncoding() + public void GZip_Ctor_NestedContent_StacksContentEncoding() { var inner = new GZipCompressedContent(new ByteArrayContent(Array.Empty())); - var outer = new BrotliCompressedContent(inner); + var outer = new GZipCompressedContent(inner); - Assert.Equal(new[] { "gzip", "br" }, outer.Headers.ContentEncoding); + Assert.Equal(new[] { "gzip", "gzip" }, outer.Headers.ContentEncoding); } [Fact] - public void Dispose_DisposesInnerContent() + public void GZip_Dispose_DisposesInnerContent() { var inner = new MockContent(); var content = new GZipCompressedContent(inner); @@ -92,68 +104,53 @@ public void Dispose_DisposesInnerContent() } [Theory] - [MemberData(nameof(RoundTripData))] - public async Task SerializeToStream_RoundTrips_MatchesOriginal(string encoding, bool async) + [InlineData(true)] + [InlineData(false)] + public async Task GZip_SerializeToStream_RoundTrips_MatchesOriginal(bool async) { byte[] original = Encoding.UTF8.GetBytes("The quick brown fox jumps over the lazy dog. " + new string('a', 1024)); - HttpContent content = CreateContent(encoding, new ByteArrayContent(original)); + var content = new GZipCompressedContent(new ByteArrayContent(original)); byte[] compressed = await SerializeAsync(content, async); - byte[] decompressed = Decompress(compressed, encoding); - Assert.Equal(original, decompressed); + Assert.Equal(original, DecompressGZip(compressed)); } [Fact] - public async Task SerializeToStream_WithOptions_RoundTrips() + public async Task GZip_SerializeToStream_WithOptions_RoundTrips() { byte[] original = Encoding.UTF8.GetBytes(new string('a', 4096)); var gzip = new GZipCompressedContent(new ByteArrayContent(original), new ZLibCompressionOptions { CompressionLevel = 5 }); - Assert.Equal(original, Decompress(await SerializeAsync(gzip, async: true), "gzip")); - - if (!OperatingSystem.IsBrowser() && !OperatingSystem.IsWasi()) - { - var brotli = new BrotliCompressedContent(new ByteArrayContent(original), new BrotliCompressionOptions { Quality = 5 }); - Assert.Equal(original, Decompress(await SerializeAsync(brotli, async: true), "br")); - var zstd = new ZstandardCompressedContent(new ByteArrayContent(original), new ZstandardCompressionOptions { Quality = 5 }); - Assert.Equal(original, Decompress(await SerializeAsync(zstd, async: true), "zstd")); - } + Assert.Equal(original, DecompressGZip(await SerializeAsync(gzip, async: true))); } - [Theory] - [MemberData(nameof(Encodings))] - public async Task SerializeToStream_WithCompressionLevel_RoundTrips(string encoding) + [Fact] + public async Task GZip_SerializeToStream_WithCompressionLevel_RoundTrips() { byte[] original = Encoding.UTF8.GetBytes(new string('a', 4096)); - HttpContent content = encoding switch - { - "gzip" => new GZipCompressedContent(new ByteArrayContent(original), CompressionLevel.SmallestSize), - "br" => new BrotliCompressedContent(new ByteArrayContent(original), CompressionLevel.SmallestSize), - "zstd" => new ZstandardCompressedContent(new ByteArrayContent(original), CompressionLevel.SmallestSize), - _ => throw new ArgumentOutOfRangeException(nameof(encoding)) - }; - Assert.Equal(original, Decompress(await SerializeAsync(content, async: true), encoding)); + var content = new GZipCompressedContent(new ByteArrayContent(original), CompressionLevel.SmallestSize); + + Assert.Equal(original, DecompressGZip(await SerializeAsync(content, async: true))); } - [Theory] - [MemberData(nameof(Encodings))] - public async Task SerializeToStream_RepeatableInnerContent_CanSerializeMultipleTimes(string encoding) + [Fact] + public async Task GZip_SerializeToStream_RepeatableInnerContent_CanSerializeMultipleTimes() { byte[] original = Encoding.UTF8.GetBytes("The quick brown fox jumps over the lazy dog. " + new string('a', 512)); - HttpContent content = CreateContent(encoding, new ByteArrayContent(original)); + var content = new GZipCompressedContent(new ByteArrayContent(original)); byte[] first = await SerializeAsync(content, async: true); byte[] second = await SerializeAsync(content, async: false); - Assert.Equal(original, Decompress(first, encoding)); - Assert.Equal(original, Decompress(second, encoding)); + Assert.Equal(original, DecompressGZip(first)); + Assert.Equal(original, DecompressGZip(second)); } [Fact] - public void SerializeToStream_NonRepeatableInnerContent_SecondCallThrows() + public void GZip_SerializeToStream_NonRepeatableInnerContent_SecondCallThrows() { var inner = new StreamContent(new NonSeekableStream(Encoding.UTF8.GetBytes("data"))); var content = new GZipCompressedContent(inner); @@ -164,41 +161,6 @@ public void SerializeToStream_NonRepeatableInnerContent_SecondCallThrows() Assert.Throws(() => content.CopyTo(ms, null, default)); } - public static IEnumerable RoundTripData() - { - foreach (bool async in new[] { true, false }) - { - yield return new object[] { "gzip", async }; - - // BrotliStream and ZstandardStream are not supported on browser/wasi. - if (!OperatingSystem.IsBrowser() && !OperatingSystem.IsWasi()) - { - yield return new object[] { "br", async }; - yield return new object[] { "zstd", async }; - } - } - } - - public static IEnumerable Encodings() - { - yield return new object[] { "gzip" }; - - // BrotliStream and ZstandardStream are not supported on browser/wasi. - if (!OperatingSystem.IsBrowser() && !OperatingSystem.IsWasi()) - { - yield return new object[] { "br" }; - yield return new object[] { "zstd" }; - } - } - - private static HttpContent CreateContent(string encoding, HttpContent inner) => encoding switch - { - "gzip" => new GZipCompressedContent(inner), - "br" => new BrotliCompressedContent(inner), - "zstd" => new ZstandardCompressedContent(inner), - _ => throw new ArgumentOutOfRangeException(nameof(encoding)) - }; - private static async Task SerializeAsync(HttpContent content, bool async) { using var ms = new MemoryStream(); @@ -215,17 +177,10 @@ private static async Task SerializeAsync(HttpContent content, bool async return ms.ToArray(); } - private static byte[] Decompress(byte[] compressed, string encoding) + private static byte[] DecompressGZip(byte[] compressed) { using var source = new MemoryStream(compressed); - using Stream decompressor = encoding switch - { - "gzip" => new GZipStream(source, CompressionMode.Decompress), - "br" => new BrotliStream(source, CompressionMode.Decompress), - "zstd" => new ZstandardStream(source, CompressionMode.Decompress), - _ => throw new ArgumentOutOfRangeException(nameof(encoding)) - }; - + using var decompressor = new GZipStream(source, CompressionMode.Decompress); using var result = new MemoryStream(); decompressor.CopyTo(result); return result.ToArray(); diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj b/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj index 957799447fcf0c..836e575358df06 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj @@ -145,7 +145,8 @@ Link="Common\System\Threading\Tasks\TaskTimeoutExtensions.cs" /> - + + Date: Wed, 15 Jul 2026 10:53:25 +0200 Subject: [PATCH 19/21] Always use TryAddWithoutValidation --- .../src/System/Net/Http/CompressedContentCore.cs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContentCore.cs b/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContentCore.cs index 792f0d1d0a541c..b3dfa35ed1ee98 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContentCore.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContentCore.cs @@ -40,16 +40,9 @@ public static void InitializeHeaders(HttpContent target, HttpContent source, str target.Headers.AddHeaders(source.Headers); // Append our encoding to the Content-Encoding header (supports stacking per HTTP spec). - // The 99% case is that the inner content has no Content-Encoding, so take the fast path - // that allocates less and only materialize the collection when we actually need to stack. - if (target.Headers.Contains(KnownHeaders.ContentEncoding.Descriptor)) - { - target.Headers.ContentEncoding.Add(encoding); - } - else - { - target.Headers.TryAddWithoutValidation(KnownHeaders.ContentEncoding.Descriptor, encoding); - } + // Always use TryAddWithoutValidation so we append the new encoding without re-parsing or + // validating any existing Content-Encoding values the inner content may have added. + target.Headers.TryAddWithoutValidation(KnownHeaders.ContentEncoding.Descriptor, encoding); // Remove Content-Length since we don't know the compressed size upfront. target.Headers.ContentLength = null; From c6584a8bf73d027873b1fcafb68936d2c662effe Mon Sep 17 00:00:00 2001 From: iremyux Date: Wed, 15 Jul 2026 11:26:35 +0200 Subject: [PATCH 20/21] Make CompressedContentCore static --- .../Net/Http/BrotliCompressedContent.cs | 14 ++++----- .../System/Net/Http/CompressedContentCore.cs | 30 +++++++------------ .../System/Net/Http/GZipCompressedContent.cs | 14 ++++----- .../Net/Http/ZstandardCompressedContent.cs | 14 ++++----- 4 files changed, 31 insertions(+), 41 deletions(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/BrotliCompressedContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/BrotliCompressedContent.cs index 7604077543aec1..0ac4ea4df98899 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/BrotliCompressedContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/BrotliCompressedContent.cs @@ -18,7 +18,7 @@ public sealed class BrotliCompressedContent : HttpContent { private const string Encoding = "br"; - private readonly CompressedContentCore _core; + private readonly HttpContent _content; private readonly BrotliCompressionOptions? _compressionOptions; private readonly CompressionLevel _compressionLevel; @@ -34,7 +34,7 @@ public BrotliCompressedContent(HttpContent content, CompressionLevel compression CompressedContentCore.ValidateCompressionLevel(compressionLevel, nameof(compressionLevel)); _compressionLevel = compressionLevel; - _core = new CompressedContentCore(content, CreateCompressionStream); + _content = content; CompressedContentCore.InitializeHeaders(this, content, Encoding); } @@ -50,18 +50,18 @@ public BrotliCompressedContent(HttpContent content, BrotliCompressionOptions com ArgumentNullException.ThrowIfNull(compressionOptions); _compressionOptions = compressionOptions; - _core = new CompressedContentCore(content, CreateCompressionStream); + _content = content; CompressedContentCore.InitializeHeaders(this, content, Encoding); } protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) => - _core.SerializeToStream(stream, context, cancellationToken); + CompressedContentCore.SerializeToStream(_content, CreateCompressionStream(stream), context, cancellationToken); protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) => - _core.SerializeToStreamAsync(stream, context, CancellationToken.None); + CompressedContentCore.SerializeToStreamAsync(_content, CreateCompressionStream(stream), context, CancellationToken.None); protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) => - _core.SerializeToStreamAsync(stream, context, cancellationToken); + CompressedContentCore.SerializeToStreamAsync(_content, CreateCompressionStream(stream), context, cancellationToken); protected internal override bool TryComputeLength(out long length) { @@ -76,7 +76,7 @@ protected override void Dispose(bool disposing) { if (disposing) { - _core.Dispose(); + _content.Dispose(); } base.Dispose(disposing); diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContentCore.cs b/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContentCore.cs index b3dfa35ed1ee98..234db253d43737 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContentCore.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContentCore.cs @@ -10,20 +10,11 @@ namespace System.Net.Http { /// - /// Shared implementation for the per-algorithm compressed content types. Holds the inner content and - /// drives serialization through a caller-provided factory that wraps the output stream in a compression stream. + /// Shared helpers for the per-algorithm compressed content types. Provides header initialization, + /// argument validation, and serialization that copies the inner content through a compression stream. /// - internal sealed class CompressedContentCore + internal static class CompressedContentCore { - private readonly HttpContent _originalContent; - private readonly Func _createCompressionStream; - - public CompressedContentCore(HttpContent originalContent, Func createCompressionStream) - { - _originalContent = originalContent; - _createCompressionStream = createCompressionStream; - } - public static void ValidateCompressionLevel(CompressionLevel compressionLevel, string paramName) { // Validate up front so an invalid enum value fails fast at construction time rather than @@ -48,21 +39,20 @@ public static void InitializeHeaders(HttpContent target, HttpContent source, str target.Headers.ContentLength = null; } - public void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) + public static void SerializeToStream(HttpContent originalContent, Stream compressionStream, TransportContext? context, CancellationToken cancellationToken) { - using Stream compressionStream = _createCompressionStream(stream); - _originalContent.CopyTo(compressionStream, context, cancellationToken); + using (compressionStream) + { + originalContent.CopyTo(compressionStream, context, cancellationToken); + } } - public async Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) + public static async Task SerializeToStreamAsync(HttpContent originalContent, Stream compressionStream, TransportContext? context, CancellationToken cancellationToken) { - Stream compressionStream = _createCompressionStream(stream); await using (compressionStream.ConfigureAwait(false)) { - await _originalContent.CopyToAsync(compressionStream, context, cancellationToken).ConfigureAwait(false); + await originalContent.CopyToAsync(compressionStream, context, cancellationToken).ConfigureAwait(false); } } - - public void Dispose() => _originalContent.Dispose(); } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/GZipCompressedContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/GZipCompressedContent.cs index ff6342f1ef7e19..d0764b2470cb86 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/GZipCompressedContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/GZipCompressedContent.cs @@ -15,7 +15,7 @@ public sealed class GZipCompressedContent : HttpContent { private const string Encoding = "gzip"; - private readonly CompressedContentCore _core; + private readonly HttpContent _content; private readonly ZLibCompressionOptions? _compressionOptions; private readonly CompressionLevel _compressionLevel; @@ -31,7 +31,7 @@ public GZipCompressedContent(HttpContent content, CompressionLevel compressionLe CompressedContentCore.ValidateCompressionLevel(compressionLevel, nameof(compressionLevel)); _compressionLevel = compressionLevel; - _core = new CompressedContentCore(content, CreateCompressionStream); + _content = content; CompressedContentCore.InitializeHeaders(this, content, Encoding); } @@ -47,18 +47,18 @@ public GZipCompressedContent(HttpContent content, ZLibCompressionOptions compres ArgumentNullException.ThrowIfNull(compressionOptions); _compressionOptions = compressionOptions; - _core = new CompressedContentCore(content, CreateCompressionStream); + _content = content; CompressedContentCore.InitializeHeaders(this, content, Encoding); } protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) => - _core.SerializeToStream(stream, context, cancellationToken); + CompressedContentCore.SerializeToStream(_content, CreateCompressionStream(stream), context, cancellationToken); protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) => - _core.SerializeToStreamAsync(stream, context, CancellationToken.None); + CompressedContentCore.SerializeToStreamAsync(_content, CreateCompressionStream(stream), context, CancellationToken.None); protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) => - _core.SerializeToStreamAsync(stream, context, cancellationToken); + CompressedContentCore.SerializeToStreamAsync(_content, CreateCompressionStream(stream), context, cancellationToken); protected internal override bool TryComputeLength(out long length) { @@ -73,7 +73,7 @@ protected override void Dispose(bool disposing) { if (disposing) { - _core.Dispose(); + _content.Dispose(); } base.Dispose(disposing); diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/ZstandardCompressedContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/ZstandardCompressedContent.cs index 460b1147b13249..20cb0b050db4c5 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/ZstandardCompressedContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/ZstandardCompressedContent.cs @@ -18,7 +18,7 @@ public sealed class ZstandardCompressedContent : HttpContent { private const string Encoding = "zstd"; - private readonly CompressedContentCore _core; + private readonly HttpContent _content; private readonly ZstandardCompressionOptions? _compressionOptions; private readonly CompressionLevel _compressionLevel; @@ -34,7 +34,7 @@ public ZstandardCompressedContent(HttpContent content, CompressionLevel compress CompressedContentCore.ValidateCompressionLevel(compressionLevel, nameof(compressionLevel)); _compressionLevel = compressionLevel; - _core = new CompressedContentCore(content, CreateCompressionStream); + _content = content; CompressedContentCore.InitializeHeaders(this, content, Encoding); } @@ -50,18 +50,18 @@ public ZstandardCompressedContent(HttpContent content, ZstandardCompressionOptio ArgumentNullException.ThrowIfNull(compressionOptions); _compressionOptions = compressionOptions; - _core = new CompressedContentCore(content, CreateCompressionStream); + _content = content; CompressedContentCore.InitializeHeaders(this, content, Encoding); } protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) => - _core.SerializeToStream(stream, context, cancellationToken); + CompressedContentCore.SerializeToStream(_content, CreateCompressionStream(stream), context, cancellationToken); protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) => - _core.SerializeToStreamAsync(stream, context, CancellationToken.None); + CompressedContentCore.SerializeToStreamAsync(_content, CreateCompressionStream(stream), context, CancellationToken.None); protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) => - _core.SerializeToStreamAsync(stream, context, cancellationToken); + CompressedContentCore.SerializeToStreamAsync(_content, CreateCompressionStream(stream), context, cancellationToken); protected internal override bool TryComputeLength(out long length) { @@ -76,7 +76,7 @@ protected override void Dispose(bool disposing) { if (disposing) { - _core.Dispose(); + _content.Dispose(); } base.Dispose(disposing); From 81d7b77e2e50ebb976194a8cde10436eb9862939 Mon Sep 17 00:00:00 2001 From: Irem Yuksel <113098562+iremyux@users.noreply.github.com> Date: Wed, 15 Jul 2026 11:39:54 +0200 Subject: [PATCH 21/21] Explicit check for CompressionLEvel Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../src/System/Net/Http/CompressedContentCore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContentCore.cs b/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContentCore.cs index 234db253d43737..96b7b13a4ff80a 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContentCore.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/CompressedContentCore.cs @@ -19,7 +19,7 @@ public static void ValidateCompressionLevel(CompressionLevel compressionLevel, s { // Validate up front so an invalid enum value fails fast at construction time rather than // deferring the exception to the serialization path when the request is being sent. - if (compressionLevel is < CompressionLevel.Optimal or > CompressionLevel.SmallestSize) + if (compressionLevel is not (CompressionLevel.Optimal or CompressionLevel.Fastest or CompressionLevel.NoCompression or CompressionLevel.SmallestSize)) { throw new ArgumentOutOfRangeException(paramName); }