From 4d78ef35a2669e9b04e68b0d1a69e0fff1b3da75 Mon Sep 17 00:00:00 2001 From: iremyux Date: Tue, 30 Jun 2026 16:31:51 +0200 Subject: [PATCH 01/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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 {