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 20cb0b050db4c5..47f36bb5692fcc 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,6 +18,17 @@ public sealed class ZstandardCompressedContent : HttpContent
{
private const string Encoding = "zstd";
+ // RFC 9659 requires zstd decoders for the "zstd" content coding to support a window size of at
+ // least 8 MB (2^23) and recommends that encoders not produce frames requiring a larger window.
+ // Some compression levels (notably CompressionLevel.SmallestSize) would otherwise select a larger
+ // window, producing payloads that a conformant server would reject. See RFC 9659, Section 3.
+ private const int RfcMaxWindowLog2 = 23;
+ private static readonly ZstandardCompressionOptions s_smallestSizeRfcOptions = new ZstandardCompressionOptions
+ {
+ Quality = ZstandardCompressionOptions.MaxQuality,
+ WindowLog2 = RfcMaxWindowLog2
+ };
+
private readonly HttpContent _content;
private readonly ZstandardCompressionOptions? _compressionOptions;
private readonly CompressionLevel _compressionLevel;
@@ -28,12 +39,27 @@ public sealed class ZstandardCompressedContent : HttpContent
///
/// The HTTP content to compress.
/// One of the enumeration values that indicates whether to emphasize speed or compression efficiency.
+ ///
+ /// RFC 9659 requires that the "zstd" content coding be decodable with a window size of 8 MB (2^23) and
+ /// recommends that encoders not produce frames requiring a larger window. When setting
+ /// to , this class applies RFC-compliant options to limit the window size
+ /// so that the produced content is accepted by servers that enforce this limit.
+ ///
public ZstandardCompressedContent(HttpContent content, CompressionLevel compressionLevel = CompressionLevel.Optimal)
{
ArgumentNullException.ThrowIfNull(content);
CompressedContentCore.ValidateCompressionLevel(compressionLevel, nameof(compressionLevel));
- _compressionLevel = compressionLevel;
+ if (compressionLevel == CompressionLevel.SmallestSize)
+ {
+ // use RFC-compliant options for SmallestSize to avoid producing frames that a conformant server would reject
+ _compressionOptions = s_smallestSizeRfcOptions;
+ }
+ else
+ {
+ _compressionLevel = compressionLevel;
+ }
+
_content = content;
CompressedContentCore.InitializeHeaders(this, content, Encoding);
}
@@ -44,6 +70,12 @@ public ZstandardCompressedContent(HttpContent content, CompressionLevel compress
///
/// The HTTP content to compress.
/// The options used to fine-tune the compression.
+ ///
+ /// RFC 9659 requires that the "zstd" content coding be decodable with a window size of 8 MB (2^23) and
+ /// recommends that encoders not produce frames requiring a larger window. When supplying custom options,
+ /// consider limiting to 23 or less so that the
+ /// produced content is accepted by servers that enforce this limit.
+ ///
public ZstandardCompressedContent(HttpContent content, ZstandardCompressionOptions compressionOptions)
{
ArgumentNullException.ThrowIfNull(content);
diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.NonBrowser.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.NonBrowser.cs
index c2ac84bfa99f69..4974075e2a035a 100644
--- a/src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.NonBrowser.cs
+++ b/src/libraries/System.Net.Http/tests/FunctionalTests/CompressedContentTest.NonBrowser.cs
@@ -3,10 +3,12 @@
using System.IO;
using System.IO.Compression;
+using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;
+using Microsoft.DotNet.XUnitExtensions;
namespace System.Net.Http.Functional.Tests
{
@@ -104,15 +106,29 @@ public async Task BrotliZstd_SerializeToStream_WithOptions_RoundTrips(string enc
Assert.Equal(original, DecompressBrotliOrZstd(await SerializeAsync(content, async: true), encoding));
}
- [Theory]
- [InlineData("br")]
- [InlineData("zstd")]
- public async Task BrotliZstd_SerializeToStream_WithCompressionLevel_RoundTrips(string encoding)
+ [ConditionalTheory]
+ [InlineData("br", CompressionLevel.NoCompression)]
+ [InlineData("br", CompressionLevel.Fastest)]
+ [InlineData("br", CompressionLevel.Optimal)]
+ [InlineData("br", CompressionLevel.SmallestSize)]
+ [InlineData("zstd", CompressionLevel.NoCompression)]
+ [InlineData("zstd", CompressionLevel.Fastest)]
+ [InlineData("zstd", CompressionLevel.Optimal)]
+ [InlineData("zstd", CompressionLevel.SmallestSize)]
+ public async Task BrotliZstd_SerializeToStream_WithCompressionLevel_RoundTrips(string encoding, CompressionLevel compressionLevel)
{
- byte[] original = Encoding.UTF8.GetBytes(new string('a', 4096));
+ if (PlatformDetection.Is32BitProcess && compressionLevel == CompressionLevel.SmallestSize && encoding == "zstd")
+ {
+ // Zstandard smallest size requires too much working memory
+ // (800+ MB) and causes intermittent allocation errors on 32-bit
+ // processes in CI.
+ throw new SkipTestException($"Skipping {encoding} with {compressionLevel} on 32-bit process due to excessive memory requirements.");
+ }
+
+ byte[] original = Encoding.UTF8.GetBytes(string.Concat(Enumerable.Repeat("The quick brown fox jumps over the lazy dog. ", 4096)));
HttpContent content = encoding == "br"
- ? new BrotliCompressedContent(new ByteArrayContent(original), CompressionLevel.SmallestSize)
- : new ZstandardCompressedContent(new ByteArrayContent(original), CompressionLevel.SmallestSize);
+ ? new BrotliCompressedContent(new ByteArrayContent(original), compressionLevel)
+ : new ZstandardCompressedContent(new ByteArrayContent(original), compressionLevel);
Assert.Equal(original, DecompressBrotliOrZstd(await SerializeAsync(content, async: true), encoding));
}
@@ -145,7 +161,8 @@ private static byte[] DecompressBrotliOrZstd(byte[] compressed, string encoding)
using Stream decompressor = encoding switch
{
"br" => new BrotliStream(source, CompressionMode.Decompress),
- "zstd" => new ZstandardStream(source, CompressionMode.Decompress),
+ // RFC 9659 requires the "zstd" content coding to be decodable with an 8 MB (2^23) window.
+ "zstd" => new ZstandardStream(source, new ZstandardDecompressionOptions { MaxWindowLog2 = 23 }),
_ => throw new ArgumentOutOfRangeException(nameof(encoding))
};