Skip to content

Commit 5c202f2

Browse files
iremyuxCopilot
andauthored
Http Request Compression (#130082)
Adds built-in support for compressing HTTP request content by introducing three new `HttpContent` types: `GZipCompressedContent`, `BrotliCompressedContent`, and `ZstandardCompressedContent`. Each type wraps an inner `HttpContent`, sets the appropriate `Content-Encoding`, clears `Content-Length`, and compresses the body as it is serialized. Shared header-copying, validation, and serialization logic is factored into an internal `CompressedContentCore` helper. Each type offers two constructors: •  `(HttpContent content, CompressionLevel compressionLevel = CompressionLevel.Optimal)`  : the high-level speed/size knob; the level is validated up front so an invalid value fails fast at construction rather than during send. •  `(HttpContent content, <Algorithm>CompressionOptions compressionOptions)`  : for fine-grained tuning. Implements #46944 --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 17bbb20 commit 5c202f2

10 files changed

Lines changed: 739 additions & 0 deletions

src/libraries/System.Net.Http/ref/System.Net.Http.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@
66

77
namespace System.Net.Http
88
{
9+
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
10+
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("wasi")]
11+
public sealed partial class BrotliCompressedContent : System.Net.Http.HttpContent
12+
{
13+
public BrotliCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.BrotliCompressionOptions compressionOptions) { }
14+
public BrotliCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.CompressionLevel compressionLevel = System.IO.Compression.CompressionLevel.Optimal) { }
15+
protected override void Dispose(bool disposing) { }
16+
protected override void SerializeToStream(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { }
17+
protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context) { throw null; }
18+
protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { throw null; }
19+
protected internal override bool TryComputeLength(out long length) { throw null; }
20+
}
921
public partial class ByteArrayContent : System.Net.Http.HttpContent
1022
{
1123
public ByteArrayContent(byte[] content) { }
@@ -42,6 +54,16 @@ public FormUrlEncodedContent(
4254
>> nameValueCollection) : base (default(byte[])) { }
4355
protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { throw null; }
4456
}
57+
public sealed partial class GZipCompressedContent : System.Net.Http.HttpContent
58+
{
59+
public GZipCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.CompressionLevel compressionLevel = System.IO.Compression.CompressionLevel.Optimal) { }
60+
public GZipCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.ZLibCompressionOptions compressionOptions) { }
61+
protected override void Dispose(bool disposing) { }
62+
protected override void SerializeToStream(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { }
63+
protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context) { throw null; }
64+
protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { throw null; }
65+
protected internal override bool TryComputeLength(out long length) { throw null; }
66+
}
4567
public delegate System.Text.Encoding? HeaderEncodingSelector<TContext>(string headerName, TContext context);
4668
public partial class HttpClient : System.Net.Http.HttpMessageInvoker
4769
{
@@ -497,6 +519,18 @@ public StringContent(string content, System.Text.Encoding? encoding, System.Net.
497519
public StringContent(string content, System.Text.Encoding? encoding, string? mediaType) : base (default(byte[])) { }
498520
protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { throw null; }
499521
}
522+
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
523+
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("wasi")]
524+
public sealed partial class ZstandardCompressedContent : System.Net.Http.HttpContent
525+
{
526+
public ZstandardCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.CompressionLevel compressionLevel = System.IO.Compression.CompressionLevel.Optimal) { }
527+
public ZstandardCompressedContent(System.Net.Http.HttpContent content, System.IO.Compression.ZstandardCompressionOptions compressionOptions) { }
528+
protected override void Dispose(bool disposing) { }
529+
protected override void SerializeToStream(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { }
530+
protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context) { throw null; }
531+
protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { throw null; }
532+
protected internal override bool TryComputeLength(out long length) { throw null; }
533+
}
500534
}
501535
namespace System.Net.Http.Headers
502536
{

src/libraries/System.Net.Http/ref/System.Net.Http.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
<ItemGroup>
1212
<ProjectReference Include="$(LibrariesProjectRoot)System.Runtime\ref\System.Runtime.csproj" />
13+
<ProjectReference Include="$(LibrariesProjectRoot)System.IO.Compression\ref\System.IO.Compression.csproj" />
14+
<ProjectReference Include="$(LibrariesProjectRoot)System.IO.Compression.Brotli\ref\System.IO.Compression.Brotli.csproj" />
1315
<ProjectReference Include="$(LibrariesProjectRoot)System.Net.Primitives\ref\System.Net.Primitives.csproj" />
1416
<ProjectReference Include="$(LibrariesProjectRoot)System.Net.Sockets\ref\System.Net.Sockets.csproj" />
1517
<ProjectReference Include="$(LibrariesProjectRoot)System.Net.Security\ref\System.Net.Security.csproj" />

src/libraries/System.Net.Http/src/System.Net.Http.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@
2323
</PropertyGroup>
2424

2525
<ItemGroup Condition="'$(TargetPlatformIdentifier)' != ''">
26+
<Compile Include="System\Net\Http\BrotliCompressedContent.cs" />
2627
<Compile Include="System\Net\Http\ByteArrayContent.cs" />
2728
<Compile Include="System\Net\Http\CancellationHelper.cs" />
2829
<Compile Include="System\Net\Http\ClientCertificateOption.cs" />
30+
<Compile Include="System\Net\Http\CompressedContentCore.cs" />
2931
<Compile Include="System\Net\Http\DelegatingHandler.cs" />
3032
<Compile Include="System\Net\Http\DiagnosticsHandler.cs" />
3133
<Compile Include="System\Net\Http\DiagnosticsHandlerLoggingStrings.cs" />
@@ -34,6 +36,7 @@
3436
<Compile Include="System\Net\Http\EmptyReadStream.cs" />
3537
<Compile Include="System\Net\Http\FormUrlEncodedContent.cs" />
3638
<Compile Include="System\Net\Http\GlobalHttpSettings.cs" />
39+
<Compile Include="System\Net\Http\GZipCompressedContent.cs" />
3740
<Compile Include="System\Net\Http\HeaderEncodingSelector.cs" />
3841
<Compile Include="System\Net\Http\Headers\KnownHeader.cs" />
3942
<Compile Include="System\Net\Http\Headers\HttpHeaderType.cs" />
@@ -75,6 +78,7 @@
7578
<Compile Include="System\Net\Http\StreamContent.cs" />
7679
<Compile Include="System\Net\Http\StreamToStreamCopy.cs" />
7780
<Compile Include="System\Net\Http\StringContent.cs" />
81+
<Compile Include="System\Net\Http\ZstandardCompressedContent.cs" />
7882
<Compile Include="System\Net\Http\Headers\AuthenticationHeaderValue.cs" />
7983
<Compile Include="System\Net\Http\Headers\BaseHeaderParser.cs" />
8084
<Compile Include="System\Net\Http\Headers\ByteArrayHeaderParser.cs" />
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.IO;
5+
using System.IO.Compression;
6+
using System.Runtime.Versioning;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
10+
namespace System.Net.Http
11+
{
12+
/// <summary>
13+
/// Provides HTTP content that compresses the inner content using the Brotli content coding.
14+
/// </summary>
15+
[UnsupportedOSPlatform("browser")]
16+
[UnsupportedOSPlatform("wasi")]
17+
public sealed class BrotliCompressedContent : HttpContent
18+
{
19+
private const string Encoding = "br";
20+
21+
private readonly HttpContent _content;
22+
private readonly BrotliCompressionOptions? _compressionOptions;
23+
private readonly CompressionLevel _compressionLevel;
24+
25+
/// <summary>
26+
/// Initializes a new instance of the <see cref="BrotliCompressedContent"/> class that compresses the
27+
/// provided content using the Brotli content coding at the specified compression level.
28+
/// </summary>
29+
/// <param name="content">The HTTP content to compress.</param>
30+
/// <param name="compressionLevel">One of the enumeration values that indicates whether to emphasize speed or compression efficiency.</param>
31+
public BrotliCompressedContent(HttpContent content, CompressionLevel compressionLevel = CompressionLevel.Optimal)
32+
{
33+
ArgumentNullException.ThrowIfNull(content);
34+
CompressedContentCore.ValidateCompressionLevel(compressionLevel, nameof(compressionLevel));
35+
36+
_compressionLevel = compressionLevel;
37+
_content = content;
38+
CompressedContentCore.InitializeHeaders(this, content, Encoding);
39+
}
40+
41+
/// <summary>
42+
/// Initializes a new instance of the <see cref="BrotliCompressedContent"/> class that compresses the
43+
/// provided content using the Brotli content coding and the specified compression options.
44+
/// </summary>
45+
/// <param name="content">The HTTP content to compress.</param>
46+
/// <param name="compressionOptions">The options used to fine-tune the compression.</param>
47+
public BrotliCompressedContent(HttpContent content, BrotliCompressionOptions compressionOptions)
48+
{
49+
ArgumentNullException.ThrowIfNull(content);
50+
ArgumentNullException.ThrowIfNull(compressionOptions);
51+
52+
_compressionOptions = compressionOptions;
53+
_content = content;
54+
CompressedContentCore.InitializeHeaders(this, content, Encoding);
55+
}
56+
57+
protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) =>
58+
CompressedContentCore.SerializeToStream(_content, CreateCompressionStream(stream), context, cancellationToken);
59+
60+
protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) =>
61+
CompressedContentCore.SerializeToStreamAsync(_content, CreateCompressionStream(stream), context, CancellationToken.None);
62+
63+
protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) =>
64+
CompressedContentCore.SerializeToStreamAsync(_content, CreateCompressionStream(stream), context, cancellationToken);
65+
66+
protected internal override bool TryComputeLength(out long length)
67+
{
68+
// Compressed size is not known without actually compressing.
69+
length = 0;
70+
return false;
71+
}
72+
73+
internal override bool AllowDuplex => false;
74+
75+
protected override void Dispose(bool disposing)
76+
{
77+
if (disposing)
78+
{
79+
_content.Dispose();
80+
}
81+
82+
base.Dispose(disposing);
83+
}
84+
85+
private BrotliStream CreateCompressionStream(Stream outputStream) =>
86+
_compressionOptions is null
87+
? new BrotliStream(outputStream, _compressionLevel, leaveOpen: true)
88+
: new BrotliStream(outputStream, _compressionOptions, leaveOpen: true);
89+
}
90+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.IO;
5+
using System.IO.Compression;
6+
using System.Net.Http.Headers;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
10+
namespace System.Net.Http
11+
{
12+
/// <summary>
13+
/// Shared helpers for the per-algorithm compressed content types. Provides header initialization,
14+
/// argument validation, and serialization that copies the inner content through a compression stream.
15+
/// </summary>
16+
internal static class CompressedContentCore
17+
{
18+
public static void ValidateCompressionLevel(CompressionLevel compressionLevel, string paramName)
19+
{
20+
// Validate up front so an invalid enum value fails fast at construction time rather than
21+
// deferring the exception to the serialization path when the request is being sent.
22+
if (compressionLevel is not (CompressionLevel.Optimal or CompressionLevel.Fastest or CompressionLevel.NoCompression or CompressionLevel.SmallestSize))
23+
{
24+
throw new ArgumentOutOfRangeException(paramName);
25+
}
26+
}
27+
28+
public static void InitializeHeaders(HttpContent target, HttpContent source, string encoding)
29+
{
30+
// Copy headers from the original content.
31+
target.Headers.AddHeaders(source.Headers);
32+
33+
// Append our encoding to the Content-Encoding header (supports stacking per HTTP spec).
34+
// Always use TryAddWithoutValidation so we append the new encoding without re-parsing or
35+
// validating any existing Content-Encoding values the inner content may have added.
36+
target.Headers.TryAddWithoutValidation(KnownHeaders.ContentEncoding.Descriptor, encoding);
37+
38+
// Remove Content-Length since we don't know the compressed size upfront.
39+
target.Headers.ContentLength = null;
40+
}
41+
42+
public static void SerializeToStream(HttpContent originalContent, Stream compressionStream, TransportContext? context, CancellationToken cancellationToken)
43+
{
44+
using (compressionStream)
45+
{
46+
originalContent.CopyTo(compressionStream, context, cancellationToken);
47+
}
48+
}
49+
50+
public static async Task SerializeToStreamAsync(HttpContent originalContent, Stream compressionStream, TransportContext? context, CancellationToken cancellationToken)
51+
{
52+
await using (compressionStream.ConfigureAwait(false))
53+
{
54+
await originalContent.CopyToAsync(compressionStream, context, cancellationToken).ConfigureAwait(false);
55+
}
56+
}
57+
}
58+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.IO;
5+
using System.IO.Compression;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
9+
namespace System.Net.Http
10+
{
11+
/// <summary>
12+
/// Provides HTTP content that compresses the inner content using the gzip content coding.
13+
/// </summary>
14+
public sealed class GZipCompressedContent : HttpContent
15+
{
16+
private const string Encoding = "gzip";
17+
18+
private readonly HttpContent _content;
19+
private readonly ZLibCompressionOptions? _compressionOptions;
20+
private readonly CompressionLevel _compressionLevel;
21+
22+
/// <summary>
23+
/// Initializes a new instance of the <see cref="GZipCompressedContent"/> class that compresses the
24+
/// provided content using the gzip content coding at the specified compression level.
25+
/// </summary>
26+
/// <param name="content">The HTTP content to compress.</param>
27+
/// <param name="compressionLevel">One of the enumeration values that indicates whether to emphasize speed or compression efficiency.</param>
28+
public GZipCompressedContent(HttpContent content, CompressionLevel compressionLevel = CompressionLevel.Optimal)
29+
{
30+
ArgumentNullException.ThrowIfNull(content);
31+
CompressedContentCore.ValidateCompressionLevel(compressionLevel, nameof(compressionLevel));
32+
33+
_compressionLevel = compressionLevel;
34+
_content = content;
35+
CompressedContentCore.InitializeHeaders(this, content, Encoding);
36+
}
37+
38+
/// <summary>
39+
/// Initializes a new instance of the <see cref="GZipCompressedContent"/> class that compresses the
40+
/// provided content using the gzip content coding and the specified compression options.
41+
/// </summary>
42+
/// <param name="content">The HTTP content to compress.</param>
43+
/// <param name="compressionOptions">The options used to fine-tune the compression.</param>
44+
public GZipCompressedContent(HttpContent content, ZLibCompressionOptions compressionOptions)
45+
{
46+
ArgumentNullException.ThrowIfNull(content);
47+
ArgumentNullException.ThrowIfNull(compressionOptions);
48+
49+
_compressionOptions = compressionOptions;
50+
_content = content;
51+
CompressedContentCore.InitializeHeaders(this, content, Encoding);
52+
}
53+
54+
protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) =>
55+
CompressedContentCore.SerializeToStream(_content, CreateCompressionStream(stream), context, cancellationToken);
56+
57+
protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) =>
58+
CompressedContentCore.SerializeToStreamAsync(_content, CreateCompressionStream(stream), context, CancellationToken.None);
59+
60+
protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) =>
61+
CompressedContentCore.SerializeToStreamAsync(_content, CreateCompressionStream(stream), context, cancellationToken);
62+
63+
protected internal override bool TryComputeLength(out long length)
64+
{
65+
// Compressed size is not known without actually compressing.
66+
length = 0;
67+
return false;
68+
}
69+
70+
internal override bool AllowDuplex => false;
71+
72+
protected override void Dispose(bool disposing)
73+
{
74+
if (disposing)
75+
{
76+
_content.Dispose();
77+
}
78+
79+
base.Dispose(disposing);
80+
}
81+
82+
private GZipStream CreateCompressionStream(Stream outputStream) =>
83+
_compressionOptions is null
84+
? new GZipStream(outputStream, _compressionLevel, leaveOpen: true)
85+
: new GZipStream(outputStream, _compressionOptions, leaveOpen: true);
86+
}
87+
}

0 commit comments

Comments
 (0)