Skip to content

Commit

Permalink
Add Brotli support to SocketsHttpHandler (dotnet/corefx#29729)
Browse files Browse the repository at this point in the history
* Add Brotli support to SocketsHttpHandler

- Add DecompressionMethods.Brotli
- Add support for Brotli to SocketsHttpHandler
- Add some tests

* Address PR feedback


Commit migrated from dotnet/corefx@91f2cd7
  • Loading branch information
stephentoub authored May 17, 2018
1 parent 2ab3783 commit 707acea
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 13 deletions.
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 @@ -602,6 +602,7 @@
<Reference Include="System.Diagnostics.Tools" />
<Reference Include="System.Diagnostics.Tracing" />
<Reference Include="System.IO.Compression" />
<Reference Include="System.IO.Compression.Brotli" />
<Reference Include="System.Memory" />
<Reference Include="System.Net.NetworkInformation" />
<Reference Include="System.Net.Primitives" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ internal sealed class DecompressionHandler : HttpMessageHandler

private const string s_gzip = "gzip";
private const string s_deflate = "deflate";
private const string s_brotli = "br";
private static readonly StringWithQualityHeaderValue s_gzipHeaderValue = new StringWithQualityHeaderValue(s_gzip);
private static readonly StringWithQualityHeaderValue s_deflateHeaderValue = new StringWithQualityHeaderValue(s_deflate);
private static readonly StringWithQualityHeaderValue s_brotliHeaderValue = new StringWithQualityHeaderValue(s_brotli);

public DecompressionHandler(DecompressionMethods decompressionMethods, HttpMessageHandler innerHandler)
{
Expand All @@ -33,6 +35,7 @@ public DecompressionHandler(DecompressionMethods decompressionMethods, HttpMessa

internal bool GZipEnabled => (_decompressionMethods & DecompressionMethods.GZip) != 0;
internal bool DeflateEnabled => (_decompressionMethods & DecompressionMethods.Deflate) != 0;
internal bool BrotliEnabled => (_decompressionMethods & DecompressionMethods.Brotli) != 0;

protected internal override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
Expand All @@ -44,6 +47,10 @@ protected internal override async Task<HttpResponseMessage> SendAsync(HttpReques
{
request.Headers.AcceptEncoding.Add(s_deflateHeaderValue);
}
if (BrotliEnabled)
{
request.Headers.AcceptEncoding.Add(s_brotliHeaderValue);
}

HttpResponseMessage response = await _innerHandler.SendAsync(request, cancellationToken).ConfigureAwait(false);

Expand All @@ -64,6 +71,10 @@ protected internal override async Task<HttpResponseMessage> SendAsync(HttpReques
{
response.Content = new DeflateDecompressedContent(response.Content);
}
else if (BrotliEnabled && last == s_brotli)
{
response.Content = new BrotliDecompressedContent(response.Content);
}
}

return response;
Expand Down Expand Up @@ -167,5 +178,16 @@ public DeflateDecompressedContent(HttpContent originalContent)
protected override Stream GetDecompressedStream(Stream originalStream) =>
new DeflateStream(originalStream, CompressionMode.Decompress);
}

private sealed class BrotliDecompressedContent : DecompressedContent
{
public BrotliDecompressedContent(HttpContent originalContent) :
base(originalContent)
{
}

protected override Stream GetDecompressedStream(Stream originalStream) =>
new BrotliStream(originalStream, CompressionMode.Decompress);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Net.Test.Common;
Expand All @@ -12,34 +13,115 @@ namespace System.Net.Http.Functional.Tests
{
public abstract class HttpClientHandler_Decompression_Test : HttpClientTestBase
{
[Fact]
public async Task Brotli_DecompressesResponse_Success()
public static IEnumerable<object[]> DecompressedResponse_MethodSpecified_DecompressedContentReturned_MemberData()
{
foreach (bool specifyAllMethods in new[] { false, true })
{
yield return new object[]
{
"deflate",
new Func<Stream, Stream>(s => new DeflateStream(s, CompressionLevel.Optimal, leaveOpen: true)),
specifyAllMethods ? DecompressionMethods.Deflate : ~DecompressionMethods.None
};
yield return new object[]
{
"gzip",
new Func<Stream, Stream>(s => new GZipStream(s, CompressionLevel.Optimal, leaveOpen: true)),
specifyAllMethods ? DecompressionMethods.GZip : ~DecompressionMethods.None
};
yield return new object[]
{
"br",
new Func<Stream, Stream>(s => new BrotliStream(s, CompressionLevel.Optimal, leaveOpen: true)),
specifyAllMethods ? DecompressionMethods.Brotli : ~DecompressionMethods.None
};
}
}

[Theory]
[MemberData(nameof(DecompressedResponse_MethodSpecified_DecompressedContentReturned_MemberData))]
public async Task DecompressedResponse_MethodSpecified_DecompressedContentReturned(
string encodingName, Func<Stream, Stream> compress, DecompressionMethods methods)
{
var expectedContent = new byte[12345];
new Random(42).NextBytes(expectedContent);

await LoopbackServer.CreateClientAndServerAsync(async uri =>
{
using (HttpClient client = CreateHttpClient())
using (HttpResponseMessage response = await client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead))
using (var decodedStream = new BrotliStream(await response.Content.ReadAsStreamAsync(), CompressionMode.Decompress))
using (HttpClientHandler handler = CreateHttpClientHandler())
using (var client = new HttpClient(handler))
{
var data = new MemoryStream();
await decodedStream.CopyToAsync(data);
Assert.Equal(expectedContent, data.ToArray());
handler.AutomaticDecompression = methods;
Assert.Equal<byte>(expectedContent, await client.GetByteArrayAsync(uri));
}
}, async server =>
{
await server.AcceptConnectionAsync(async connection =>
{
await connection.ReadRequestHeaderAsync();
await connection.Writer.WriteAsync("HTTP/1.1 200 OK\r\nContent-Encoding: br\r\n\r\n");
using (var brotli = new BrotliStream(connection.Stream, CompressionLevel.Optimal, leaveOpen: true))
await connection.Writer.WriteAsync($"HTTP/1.1 200 OK\r\nContent-Encoding: {encodingName}\r\n\r\n");
using (Stream compressedStream = compress(connection.Stream))
{
await brotli.WriteAsync(expectedContent);
await compressedStream.WriteAsync(expectedContent);
}
});
});
}

public static IEnumerable<object[]> DecompressedResponse_MethodNotSpecified_OriginalContentReturned_MemberData()
{
yield return new object[]
{
"deflate",
new Func<Stream, Stream>(s => new DeflateStream(s, CompressionLevel.Optimal, leaveOpen: true)),
DecompressionMethods.None
};
yield return new object[]
{
"gzip",
new Func<Stream, Stream>(s => new GZipStream(s, CompressionLevel.Optimal, leaveOpen: true)),
DecompressionMethods.Brotli
};
yield return new object[]
{
"br",
new Func<Stream, Stream>(s => new BrotliStream(s, CompressionLevel.Optimal, leaveOpen: true)),
DecompressionMethods.Deflate | DecompressionMethods.GZip
};
}

[Theory]
[MemberData(nameof(DecompressedResponse_MethodNotSpecified_OriginalContentReturned_MemberData))]
public async Task DecompressedResponse_MethodNotSpecified_OriginalContentReturned(
string encodingName, Func<Stream, Stream> compress, DecompressionMethods methods)
{
var expectedContent = new byte[12345];
new Random(42).NextBytes(expectedContent);

var compressedContentStream = new MemoryStream();
using (Stream s = compress(compressedContentStream))
{
await s.WriteAsync(expectedContent);
}
byte[] compressedContent = compressedContentStream.ToArray();

await LoopbackServer.CreateClientAndServerAsync(async uri =>
{
using (HttpClientHandler handler = CreateHttpClientHandler())
using (var client = new HttpClient(handler))
{
handler.AutomaticDecompression = methods;
Assert.Equal<byte>(compressedContent, await client.GetByteArrayAsync(uri));
}
}, async server =>
{
await server.AcceptConnectionAsync(async connection =>
{
await connection.ReadRequestHeaderAsync();
await connection.Writer.WriteAsync($"HTTP/1.1 200 OK\r\nContent-Encoding: {encodingName}\r\n\r\n");
await connection.Stream.WriteAsync(compressedContent);
});
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ public void Remove(System.Uri uriPrefix, string authType) { }
[System.FlagsAttribute]
public enum DecompressionMethods
{
Brotli = 4,
Deflate = 2,
GZip = 1,
None = 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ namespace System.Net
public enum DecompressionMethods
{
None = 0,
GZip = 1,
Deflate = 2
GZip = 0x1,
Deflate = 0x2,
Brotli = 0x4
}
}

0 comments on commit 707acea

Please sign in to comment.