diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Decompression.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Decompression.cs index f45206e9941dfe..02724599c120ee 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Decompression.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Decompression.cs @@ -28,7 +28,7 @@ public abstract class HttpClientHandler_Decompression_Test : HttpClientHandlerTe public HttpClientHandler_Decompression_Test(ITestOutputHelper output) : base(output) { } public static IEnumerable DecompressedResponse_MethodSpecified_DecompressedContentReturned_MemberData() => - from compressionName in new[] { "gzip", "GZIP", "zlib", "ZLIB", "deflate", "DEFLATE", "br", "BR" } + from compressionName in new[] { "gzip", "GZIP", "zlib", "ZLIB", "deflate", "DEFLATE", "br", "BR", "zstd", "ZSTD" } from all in new[] { false, true } from copyTo in new[] { false, true } from contentLength in new[] { 0, 1, 12345 } @@ -40,9 +40,9 @@ public static IEnumerable DecompressedResponse_MethodSpecified_Decompr public async Task DecompressedResponse_MethodSpecified_DecompressedContentReturned(string compressionName, bool all, bool useCopyTo, int contentLength) { if (IsWinHttpHandler && - (compressionName is "br" or "BR" or "zlib" or "ZLIB")) + (compressionName is "br" or "BR" or "zlib" or "ZLIB" or "zstd" or "ZSTD")) { - // brotli and zlib not supported on WinHttpHandler + // brotli, zlib, and zstd not supported on WinHttpHandler return; } @@ -64,6 +64,12 @@ public async Task DecompressedResponse_MethodSpecified_DecompressedContentReturn methods = all ? DecompressionMethods.Brotli : _all; break; + case "zstd": + case "ZSTD": + compress = s => new ZstandardStream(s, CompressionLevel.Optimal, leaveOpen: true); + methods = all ? DecompressionMethods.Zstandard : _all; + break; + case "zlib": case "ZLIB": compress = s => new ZLibStream(s, CompressionLevel.Optimal, leaveOpen: true); @@ -133,6 +139,13 @@ public static IEnumerable DecompressedResponse_MethodNotSpecified_Orig DecompressionMethods.Deflate | DecompressionMethods.GZip, useCopyTo }; + yield return new object[] + { + "zstd", + new Func(s => new ZstandardStream(s, CompressionLevel.Optimal, leaveOpen: true)), + DecompressionMethods.Deflate | DecompressionMethods.GZip | DecompressionMethods.Brotli, + useCopyTo + }; #endif } } @@ -177,6 +190,7 @@ await server.AcceptConnectionAsync(async connection => #if !NETFRAMEWORK [InlineData("deflate", DecompressionMethods.Deflate)] [InlineData("br", DecompressionMethods.Brotli)] + [InlineData("zstd", DecompressionMethods.Zstandard)] #endif [SkipOnPlatform(TestPlatforms.Browser, "AutomaticDecompression not supported on Browser")] public async Task DecompressedResponse_EmptyBody_Success(string encodingName, DecompressionMethods methods) @@ -205,6 +219,10 @@ await server.AcceptConnectionAsync(async connection => [InlineData(DecompressionMethods.Brotli, "br", "br")] [InlineData(DecompressionMethods.Brotli, "br", "gzip")] [InlineData(DecompressionMethods.Brotli, "br", "gzip, deflate")] + [InlineData(DecompressionMethods.Zstandard, "zstd", "")] + [InlineData(DecompressionMethods.Zstandard, "zstd", "zstd")] + [InlineData(DecompressionMethods.Zstandard, "zstd", "gzip")] + [InlineData(DecompressionMethods.Zstandard, "zstd", "gzip, deflate, br")] #endif [InlineData(DecompressionMethods.GZip, "gzip", "")] [InlineData(DecompressionMethods.Deflate, "deflate", "")] @@ -228,6 +246,12 @@ public async Task GetAsync_SetAutomaticDecompression_AcceptEncodingHeaderSentWit return; } + // Zstandard only supported on SocketsHttpHandler. + if (IsWinHttpHandler && (encodings.Contains("zstd") || manualAcceptEncodingHeaderValues.Contains("zstd"))) + { + return; + } + await LoopbackServer.CreateServerAsync(async (server, url) => { HttpClientHandler handler = CreateHttpClientHandler(); @@ -266,6 +290,7 @@ await LoopbackServer.CreateServerAsync(async (server, url) => [Theory] #if NET [InlineData(DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli, "gzip; q=1.0, deflate; q=1.0, br; q=1.0", "")] + [InlineData(DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli | DecompressionMethods.Zstandard, "gzip; q=1.0, deflate; q=1.0, br; q=1.0, zstd; q=1.0", "")] #endif [InlineData(DecompressionMethods.GZip | DecompressionMethods.Deflate, "gzip; q=1.0, deflate; q=1.0", "")] [InlineData(DecompressionMethods.GZip | DecompressionMethods.Deflate, "gzip; q=1.0", "deflate")] 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 63d88d70d79ea0..fe0c89f1d9433b 100644 --- a/src/libraries/System.Net.Http/src/System.Net.Http.csproj +++ b/src/libraries/System.Net.Http/src/System.Net.Http.csproj @@ -486,6 +486,7 @@ + diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/DecompressionHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/DecompressionHandler.cs index c0e15763b53db6..f9bbdee017e3b3 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/DecompressionHandler.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/DecompressionHandler.cs @@ -19,9 +19,11 @@ internal sealed class DecompressionHandler : HttpMessageHandlerStage private const string Gzip = "gzip"; private const string Deflate = "deflate"; private const string Brotli = "br"; + private const string Zstd = "zstd"; private static readonly StringWithQualityHeaderValue s_gzipHeaderValue = new(Gzip); private static readonly StringWithQualityHeaderValue s_deflateHeaderValue = new(Deflate); private static readonly StringWithQualityHeaderValue s_brotliHeaderValue = new(Brotli); + private static readonly StringWithQualityHeaderValue s_zstdHeaderValue = new(Zstd); /// Header value for all enabled decompression methods, e.g. "gzip, deflate". private readonly string _acceptEncodingHeaderValue; @@ -34,14 +36,19 @@ public DecompressionHandler(DecompressionMethods decompressionMethods, HttpMessa _decompressionMethods = decompressionMethods; _innerHandler = innerHandler; - List methods = [GZipEnabled ? Gzip : null, DeflateEnabled ? Deflate : null, BrotliEnabled ? Brotli : null]; - methods.RemoveAll(item => item is null); - _acceptEncodingHeaderValue = string.Join(", ", methods); + Span methods = [null, null, null, null]; + int count = 0; + if (GZipEnabled) methods[count++] = Gzip; + if (DeflateEnabled) methods[count++] = Deflate; + if (BrotliEnabled) methods[count++] = Brotli; + if (ZstandardEnabled) methods[count++] = Zstd; + _acceptEncodingHeaderValue = string.Join(", ", methods.Slice(0, count)); } internal bool GZipEnabled => (_decompressionMethods & DecompressionMethods.GZip) != 0; internal bool DeflateEnabled => (_decompressionMethods & DecompressionMethods.Deflate) != 0; internal bool BrotliEnabled => (_decompressionMethods & DecompressionMethods.Brotli) != 0; + internal bool ZstandardEnabled => (_decompressionMethods & DecompressionMethods.Zstandard) != 0; private static bool EncodingExists(HttpHeaderValueCollection acceptEncodingHeader, string encoding) { @@ -81,6 +88,11 @@ internal override async ValueTask SendAsync(HttpRequestMess { acceptEncoding.Add(s_brotliHeaderValue); } + + if (ZstandardEnabled && !EncodingExists(acceptEncoding, Zstd)) + { + acceptEncoding.Add(s_zstdHeaderValue); + } } HttpResponseMessage response = await _innerHandler.SendAsync(request, async, cancellationToken).ConfigureAwait(false); @@ -105,6 +117,10 @@ internal override async ValueTask SendAsync(HttpRequestMess { response.Content = new BrotliDecompressedContent(response.Content, encodings); } + else if (ZstandardEnabled && string.Equals(last, Zstd, StringComparison.OrdinalIgnoreCase)) + { + response.Content = new ZstandardDecompressedContent(response.Content, encodings); + } } return response; @@ -430,5 +446,11 @@ private sealed class BrotliDecompressedContent(HttpContent originalContent, stri protected override Stream GetDecompressedStream(Stream originalStream) => new BrotliStream(originalStream, CompressionMode.Decompress); } + + private sealed class ZstandardDecompressedContent(HttpContent originalContent, string[] contentEncodings) : DecompressedContent(originalContent, contentEncodings) + { + protected override Stream GetDecompressedStream(Stream originalStream) => + new ZstandardStream(originalStream, CompressionMode.Decompress); + } } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandler.cs index cd27280882608f..94a3043e913aeb 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandler.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandler.cs @@ -569,7 +569,7 @@ private void EnsureDecompressionHandlerFactory() } // Not stored as a constant on the DecompressionHandler to allow it to get trimmed. - private const DecompressionMethods SupportedDecompressionMethods = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli; + private const DecompressionMethods SupportedDecompressionMethods = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli | DecompressionMethods.Zstandard; protected internal override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) diff --git a/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs b/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs index d49a06959511db..4374afe3b2a9f8 100644 --- a/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs +++ b/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs @@ -120,6 +120,7 @@ public enum DecompressionMethods GZip = 1, Deflate = 2, Brotli = 4, + Zstandard = 8, } public partial class DnsEndPoint : System.Net.EndPoint { diff --git a/src/libraries/System.Net.Primitives/src/System/Net/DecompressionMethods.cs b/src/libraries/System.Net.Primitives/src/System/Net/DecompressionMethods.cs index 5523929dc46dfa..846c87d46d00ff 100644 --- a/src/libraries/System.Net.Primitives/src/System/Net/DecompressionMethods.cs +++ b/src/libraries/System.Net.Primitives/src/System/Net/DecompressionMethods.cs @@ -10,6 +10,7 @@ public enum DecompressionMethods GZip = 0x1, Deflate = 0x2, Brotli = 0x4, + Zstandard = 0x8, All = ~None } }