Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public abstract class HttpClientHandler_Decompression_Test : HttpClientHandlerTe
public HttpClientHandler_Decompression_Test(ITestOutputHelper output) : base(output) { }

public static IEnumerable<object[]> 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 }
Expand All @@ -40,9 +40,9 @@ public static IEnumerable<object[]> 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;
}

Expand All @@ -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);
Expand Down Expand Up @@ -133,6 +139,13 @@ public static IEnumerable<object[]> DecompressedResponse_MethodNotSpecified_Orig
DecompressionMethods.Deflate | DecompressionMethods.GZip,
useCopyTo
};
yield return new object[]
{
"zstd",
new Func<Stream, Stream>(s => new ZstandardStream(s, CompressionLevel.Optimal, leaveOpen: true)),
DecompressionMethods.Deflate | DecompressionMethods.GZip | DecompressionMethods.Brotli,
useCopyTo
};
#endif
}
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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", "")]
Expand All @@ -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();
Expand Down Expand Up @@ -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")]
Expand Down
1 change: 1 addition & 0 deletions src/libraries/System.Net.Http/src/System.Net.Http.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,7 @@
<ProjectReference Include="$(LibrariesProjectRoot)System.Threading.Channels\src\System.Threading.Channels.csproj" />
<ProjectReference Include="$(LibrariesProjectRoot)System.Threading.ThreadPool\src\System.Threading.ThreadPool.csproj" />
<ProjectReference Include="$(LibrariesProjectRoot)System.IO.Compression.Brotli\src\System.IO.Compression.Brotli.csproj" />
<ProjectReference Include="$(LibrariesProjectRoot)System.IO.Compression.Zstandard\src\System.IO.Compression.Zstandard.csproj" />
</ItemGroup>

<ItemGroup Condition="'$(TargetPlatformIdentifier)' != 'browser' and '$(TargetPlatformIdentifier)' != 'wasi' ">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

/// <summary>Header value for all enabled decompression methods, e.g. "gzip, deflate".</summary>
private readonly string _acceptEncodingHeaderValue;
Expand All @@ -34,14 +36,15 @@ public DecompressionHandler(DecompressionMethods decompressionMethods, HttpMessa
_decompressionMethods = decompressionMethods;
_innerHandler = innerHandler;

List<string?> methods = [GZipEnabled ? Gzip : null, DeflateEnabled ? Deflate : null, BrotliEnabled ? Brotli : null];
List<string?> methods = [GZipEnabled ? Gzip : null, DeflateEnabled ? Deflate : null, BrotliEnabled ? Brotli : null, ZstandardEnabled ? Zstd : null];
methods.RemoveAll(item => item is null);
_acceptEncodingHeaderValue = string.Join(", ", methods);
Comment on lines +39 to 41
Copy link
Member

Choose a reason for hiding this comment

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

Span<string?> methods = ["", "", "", ""];
int count = 0;
if (GZipEnabled) methods[count++] = Gzip;
if (DeflateEnabled) methods[count++] = Deflate;
if (BrotliEnabled) methods[count++] = Brotli;
if (ZstandardEnabled) methods[count++] = Zstandard;
_acceptEncodingHeaderValue = string.Join(", ", methods.Slice(0, count);

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The comment appears incomplete - could you clarify what change you'd like to see for lines 39-41? Are you suggesting to use Span<string> with stackalloc instead of the List pattern?

Copy link
Member

Choose a reason for hiding this comment

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

@copilot, incomplete how? I'm suggesting the code should look like this:

Span<string?> methods = ["", "", "", ""];
int count = 0;
if (GZipEnabled) methods[count++] = Gzip;
if (DeflateEnabled) methods[count++] = Deflate;
if (BrotliEnabled) methods[count++] = Brotli;
if (ZstandardEnabled) methods[count++] = Zstandard;
_acceptEncodingHeaderValue = string.Join(", ", methods.Slice(0, count);

It initializes a span with a collection expression, ensuring enough space for the maximum number of encodings. Then it fills in just the encodings that are enabled. And then it concatenates those strings into a single string.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The code suggestion appears to be cut off after "Span" - could you provide the complete code you'd like to see for lines 39-41?

}

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<StringWithQualityHeaderValue> acceptEncodingHeader, string encoding)
{
Expand Down Expand Up @@ -81,6 +84,11 @@ internal override async ValueTask<HttpResponseMessage> 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);
Expand All @@ -105,6 +113,10 @@ internal override async ValueTask<HttpResponseMessage> 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;
Expand Down Expand Up @@ -430,5 +442,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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ public enum DecompressionMethods
GZip = 1,
Deflate = 2,
Brotli = 4,
Zstandard = 8,
}
public partial class DnsEndPoint : System.Net.EndPoint
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public enum DecompressionMethods
GZip = 0x1,
Deflate = 0x2,
Brotli = 0x4,
Zstandard = 0x8,
All = ~None
}
}
Loading