diff --git a/src/Microsoft.AspNetCore.ResponseCompression/Microsoft.AspNetCore.ResponseCompression.csproj b/src/Microsoft.AspNetCore.ResponseCompression/Microsoft.AspNetCore.ResponseCompression.csproj index d39f7db89a..7f594f1c30 100644 --- a/src/Microsoft.AspNetCore.ResponseCompression/Microsoft.AspNetCore.ResponseCompression.csproj +++ b/src/Microsoft.AspNetCore.ResponseCompression/Microsoft.AspNetCore.ResponseCompression.csproj @@ -9,6 +9,7 @@ + diff --git a/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionProvider.cs b/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionProvider.cs index d459c13ff3..b63ff7380e 100644 --- a/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionProvider.cs @@ -3,8 +3,12 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.ResponseCompression.Internal; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; @@ -18,6 +22,7 @@ namespace Microsoft.AspNetCore.ResponseCompression private readonly HashSet _mimeTypes; private readonly HashSet _excludedMimeTypes; private readonly bool _enableForHttps; + private readonly ILogger _logger; /// /// If no compression providers are specified then GZip is used by default. @@ -67,6 +72,7 @@ namespace Microsoft.AspNetCore.ResponseCompression { mimeTypes = ResponseCompressionDefaults.MimeTypes; } + _mimeTypes = new HashSet(mimeTypes, StringComparer.OrdinalIgnoreCase); _excludedMimeTypes = new HashSet( @@ -75,6 +81,8 @@ namespace Microsoft.AspNetCore.ResponseCompression ); _enableForHttps = responseCompressionOptions.EnableForHttps; + + _logger = services.GetRequiredService>(); } /// @@ -83,76 +91,86 @@ namespace Microsoft.AspNetCore.ResponseCompression // e.g. Accept-Encoding: gzip, deflate, sdch var accept = context.Request.Headers[HeaderNames.AcceptEncoding]; + // Note this is already checked in CheckRequestAcceptsCompression which _should_ prevent any of these other methods from being called. if (StringValues.IsNullOrEmpty(accept)) { + Debug.Assert(false, "Duplicate check failed."); + _logger.NoAcceptEncoding(); return null; } - if (StringWithQualityHeaderValue.TryParseList(accept, out var encodings)) + if (!StringWithQualityHeaderValue.TryParseList(accept, out var encodings) || !encodings.Any()) { - if (encodings.Count == 0) + _logger.NoAcceptEncoding(); + return null; + } + + var candidates = new HashSet(); + + foreach (var encoding in encodings) + { + var encodingName = encoding.Value; + var quality = encoding.Quality.GetValueOrDefault(1); + + if (quality < double.Epsilon) { - return null; + continue; } - var candidates = new HashSet(); - - foreach (var encoding in encodings) + for (int i = 0; i < _providers.Length; i++) { - var encodingName = encoding.Value; - var quality = encoding.Quality.GetValueOrDefault(1); + var provider = _providers[i]; - if (quality < double.Epsilon) + if (StringSegment.Equals(provider.EncodingName, encodingName, StringComparison.OrdinalIgnoreCase)) { - continue; + candidates.Add(new ProviderCandidate(provider.EncodingName, quality, i, provider)); } + } + // Uncommon but valid options + if (StringSegment.Equals("*", encodingName, StringComparison.Ordinal)) + { for (int i = 0; i < _providers.Length; i++) { var provider = _providers[i]; - if (StringSegment.Equals(provider.EncodingName, encodingName, StringComparison.OrdinalIgnoreCase)) - { - candidates.Add(new ProviderCandidate(provider.EncodingName, quality, i, provider)); - } + // Any provider is a candidate. + candidates.Add(new ProviderCandidate(provider.EncodingName, quality, i, provider)); } - // Uncommon but valid options - if (StringSegment.Equals("*", encodingName, StringComparison.Ordinal)) - { - for (int i = 0; i < _providers.Length; i++) - { - var provider = _providers[i]; - - // Any provider is a candidate. - candidates.Add(new ProviderCandidate(provider.EncodingName, quality, i, provider)); - } - - break; - } - - if (StringSegment.Equals("identity", encodingName, StringComparison.OrdinalIgnoreCase)) - { - // We add 'identity' to the list of "candidates" with a very low priority and no provider. - // This will allow it to be ordered based on its quality (and priority) later in the method. - candidates.Add(new ProviderCandidate(encodingName.Value, quality, priority: int.MaxValue, provider: null)); - } + break; } - if (candidates.Count <= 1) + if (StringSegment.Equals("identity", encodingName, StringComparison.OrdinalIgnoreCase)) { - return candidates.ElementAtOrDefault(0).Provider; + // We add 'identity' to the list of "candidates" with a very low priority and no provider. + // This will allow it to be ordered based on its quality (and priority) later in the method. + candidates.Add(new ProviderCandidate(encodingName.Value, quality, priority: int.MaxValue, provider: null)); } - - var accepted = candidates - .OrderByDescending(x => x.Quality) - .ThenBy(x => x.Priority) - .First(); - - return accepted.Provider; } - return null; + ICompressionProvider selectedProvider = null; + if (candidates.Count <= 1) + { + selectedProvider = candidates.FirstOrDefault().Provider; + } + else + { + selectedProvider = candidates + .OrderByDescending(x => x.Quality) + .ThenBy(x => x.Priority) + .First().Provider; + } + + if (selectedProvider == null) + { + // "identity" would match as a candidate but not have a provider implementation + _logger.NoCompressionProvider(); + return null; + } + + _logger.CompressingWith(selectedProvider.EncodingName); + return selectedProvider; } /// @@ -160,11 +178,13 @@ namespace Microsoft.AspNetCore.ResponseCompression { if (context.Response.Headers.ContainsKey(HeaderNames.ContentRange)) { + _logger.NoCompressionDueToHeader(HeaderNames.ContentRange); return false; } if (context.Response.Headers.ContainsKey(HeaderNames.ContentEncoding)) { + _logger.NoCompressionDueToHeader(HeaderNames.ContentEncoding); return false; } @@ -172,6 +192,7 @@ namespace Microsoft.AspNetCore.ResponseCompression if (string.IsNullOrEmpty(mimeType)) { + _logger.NoCompressionForContentType(mimeType); return false; } @@ -183,9 +204,18 @@ namespace Microsoft.AspNetCore.ResponseCompression mimeType = mimeType.Trim(); } - return ShouldCompressExact(mimeType) //check exact match type/subtype + var shouldCompress = ShouldCompressExact(mimeType) //check exact match type/subtype ?? ShouldCompressPartial(mimeType) //check partial match type/* ?? _mimeTypes.Contains("*/*"); //check wildcard */* + + if (shouldCompress) + { + _logger.ShouldCompressResponse(); // Trace, there will be more logs + return true; + } + + _logger.NoCompressionForContentType(mimeType); + return false; } /// @@ -193,9 +223,18 @@ namespace Microsoft.AspNetCore.ResponseCompression { if (context.Request.IsHttps && !_enableForHttps) { + _logger.NoCompressionForHttps(); return false; } - return !string.IsNullOrEmpty(context.Request.Headers[HeaderNames.AcceptEncoding]); + + if (string.IsNullOrEmpty(context.Request.Headers[HeaderNames.AcceptEncoding])) + { + _logger.NoAcceptEncoding(); + return false; + } + + _logger.RequestAcceptsCompression(); // Trace, there will be more logs + return true; } private bool? ShouldCompressExact(string mimeType) diff --git a/src/Microsoft.AspNetCore.ResponseCompression/internal/ResponseCompressionLoggingExtensions.cs b/src/Microsoft.AspNetCore.ResponseCompression/internal/ResponseCompressionLoggingExtensions.cs new file mode 100644 index 0000000000..75f3d07fd3 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCompression/internal/ResponseCompressionLoggingExtensions.cs @@ -0,0 +1,72 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.ResponseCompression.Internal +{ + internal static class ResponseCompressionLoggingExtensions + { + private static readonly Action _noAcceptEncoding; + private static readonly Action _noCompressionForHttps; + private static readonly Action _requestAcceptsCompression; + private static readonly Action _noCompressionDueToHeader; + private static readonly Action _noCompressionForContentType; + private static readonly Action _shouldCompressResponse; + private static readonly Action _noCompressionProvider; + private static readonly Action _compressWith; + + static ResponseCompressionLoggingExtensions() + { + _noAcceptEncoding = LoggerMessage.Define(LogLevel.Debug, 1, "No response compression available, the Accept-Encoding header is missing or invalid."); + _noCompressionForHttps = LoggerMessage.Define(LogLevel.Debug, 2, "No response compression available for HTTPS requests. See ResponseCompressionOptions.EnableForHttps."); + _requestAcceptsCompression = LoggerMessage.Define(LogLevel.Trace, 3, "This request accepts compression."); + _noCompressionDueToHeader = LoggerMessage.Define(LogLevel.Debug, 4, "Response compression disabled due to the {header} header."); + _noCompressionForContentType = LoggerMessage.Define(LogLevel.Debug, 5, "Response compression is not enabled for the Content-Type '{header}'."); + _shouldCompressResponse = LoggerMessage.Define(LogLevel.Trace, 6, "Response compression is available for this Content-Type."); + _noCompressionProvider = LoggerMessage.Define(LogLevel.Debug, 7, "No matching response compression provider found."); + _compressWith = LoggerMessage.Define(LogLevel.Debug, 8, "The response will be compressed with '{provider}'."); + } + + public static void NoAcceptEncoding(this ILogger logger) + { + _noAcceptEncoding(logger, null); + } + + public static void NoCompressionForHttps(this ILogger logger) + { + _noCompressionForHttps(logger, null); + } + + public static void RequestAcceptsCompression(this ILogger logger) + { + _requestAcceptsCompression(logger, null); + } + + public static void NoCompressionDueToHeader(this ILogger logger, string header) + { + _noCompressionDueToHeader(logger, header, null); + } + + public static void NoCompressionForContentType(this ILogger logger, string header) + { + _noCompressionForContentType(logger, header, null); + } + + public static void ShouldCompressResponse(this ILogger logger) + { + _shouldCompressResponse(logger, null); + } + + public static void NoCompressionProvider(this ILogger logger) + { + _noCompressionProvider(logger, null); + } + + public static void CompressingWith(this ILogger logger, string provider) + { + _compressWith(logger, provider, null); + } + } +} diff --git a/test/Microsoft.AspNetCore.ResponseCompression.Tests/Microsoft.AspNetCore.ResponseCompression.Tests.csproj b/test/Microsoft.AspNetCore.ResponseCompression.Tests/Microsoft.AspNetCore.ResponseCompression.Tests.csproj index 902449b486..b979c66a00 100644 --- a/test/Microsoft.AspNetCore.ResponseCompression.Tests/Microsoft.AspNetCore.ResponseCompression.Tests.csproj +++ b/test/Microsoft.AspNetCore.ResponseCompression.Tests/Microsoft.AspNetCore.ResponseCompression.Tests.csproj @@ -14,6 +14,7 @@ + diff --git a/test/Microsoft.AspNetCore.ResponseCompression.Tests/ResponseCompressionMiddlewareTest.cs b/test/Microsoft.AspNetCore.ResponseCompression.Tests/ResponseCompressionMiddlewareTest.cs index 08fa0b283e..8cf9d3bf1e 100644 --- a/test/Microsoft.AspNetCore.ResponseCompression.Tests/ResponseCompressionMiddlewareTest.cs +++ b/test/Microsoft.AspNetCore.ResponseCompression.Tests/ResponseCompressionMiddlewareTest.cs @@ -15,6 +15,8 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; using Microsoft.Net.Http.Headers; using Xunit; @@ -55,28 +57,31 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests [Fact] public async Task Request_NoAcceptEncoding_Uncompressed() { - var response = await InvokeMiddleware(100, requestAcceptEncodings: null, responseType: TextPlain); + var (response, logMessages) = await InvokeMiddleware(100, requestAcceptEncodings: null, responseType: TextPlain); CheckResponseNotCompressed(response, expectedBodyLength: 100, sendVaryHeader: false); + AssertLog(logMessages.Single(), LogLevel.Debug, "No response compression available, the Accept-Encoding header is missing or invalid."); } [Fact] public async Task Request_AcceptGzipDeflate_CompressedGzip() { - var response = await InvokeMiddleware(100, requestAcceptEncodings: new[] { "gzip", "deflate" }, responseType: TextPlain); + var (response, logMessages) = await InvokeMiddleware(100, requestAcceptEncodings: new[] { "gzip", "deflate" }, responseType: TextPlain); CheckResponseCompressed(response, expectedBodyLength: 24, expectedEncoding: "gzip"); + AssertCompressedWithLog(logMessages, "gzip"); } [Fact] public async Task Request_AcceptBrotli_CompressedBrotli() { - var response = await InvokeMiddleware(100, requestAcceptEncodings: new[] { "br" }, responseType: TextPlain); + var (response, logMessages) = await InvokeMiddleware(100, requestAcceptEncodings: new[] { "br" }, responseType: TextPlain); #if NET461 CheckResponseNotCompressed(response, expectedBodyLength: 100, sendVaryHeader: true); #elif NETCOREAPP2_2 CheckResponseCompressed(response, expectedBodyLength: 20, expectedEncoding: "br"); + AssertCompressedWithLog(logMessages, "br"); #else #error Target frameworks need to be updated. #endif @@ -87,12 +92,14 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests [InlineData("br", "gzip")] public async Task Request_AcceptMixed_CompressedBrotli(string encoding1, string encoding2) { - var response = await InvokeMiddleware(100, new[] { encoding1, encoding2 }, responseType: TextPlain); + var (response, logMessages) = await InvokeMiddleware(100, new[] { encoding1, encoding2 }, responseType: TextPlain); #if NET461 CheckResponseCompressed(response, expectedBodyLength: 24, expectedEncoding: "gzip"); + AssertCompressedWithLog(logMessages, "gzip"); #elif NETCOREAPP2_2 CheckResponseCompressed(response, expectedBodyLength: 20, expectedEncoding: "br"); + AssertCompressedWithLog(logMessages, "br"); #else #error Target frameworks need to be updated. #endif @@ -110,9 +117,10 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests options.Providers.Add(); } - var response = await InvokeMiddleware(100, new[] { encoding1, encoding2 }, responseType: TextPlain, configure: Configure); + var (response, logMessages) = await InvokeMiddleware(100, new[] { encoding1, encoding2 }, responseType: TextPlain, configure: Configure); CheckResponseCompressed(response, expectedBodyLength: 24, expectedEncoding: "gzip"); + AssertCompressedWithLog(logMessages, "gzip"); } #elif NET461 #else @@ -122,9 +130,13 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests [Fact] public async Task Request_AcceptUnknown_NotCompressed() { - var response = await InvokeMiddleware(100, requestAcceptEncodings: new[] { "unknown" }, responseType: TextPlain); + var (response, logMessages) = await InvokeMiddleware(100, requestAcceptEncodings: new[] { "unknown" }, responseType: TextPlain); CheckResponseNotCompressed(response, expectedBodyLength: 100, sendVaryHeader: true); + Assert.Equal(3, logMessages.Count); + AssertLog(logMessages.First(), LogLevel.Trace, "This request accepts compression."); + AssertLog(logMessages.Skip(1).First(), LogLevel.Trace, "Response compression is available for this Content-Type."); + AssertLog(logMessages.Skip(2).First(), LogLevel.Debug, "No matching response compression provider found."); } [Theory] @@ -134,31 +146,10 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests [InlineData("text/plain ; charset=ISO-8859-4")] public async Task ContentType_WithCharset_Compress(string contentType) { - var builder = new WebHostBuilder() - .ConfigureServices(services => - { - services.AddResponseCompression(); - }) - .Configure(app => - { - app.UseResponseCompression(); - app.Run(context => - { - context.Response.Headers[HeaderNames.ContentMD5] = "MD5"; - context.Response.ContentType = contentType; - return context.Response.WriteAsync(new string('a', 100)); - }); - }); - - var server = new TestServer(builder); - var client = server.CreateClient(); - - var request = new HttpRequestMessage(HttpMethod.Get, ""); - request.Headers.AcceptEncoding.ParseAdd("gzip"); - - var response = await client.SendAsync(request); + var (response, logMessages) = await InvokeMiddleware(uncompressedBodyLength: 100, requestAcceptEncodings: new[] { "gzip" }, contentType); CheckResponseCompressed(response, expectedBodyLength: 24, expectedEncoding: "gzip"); + AssertCompressedWithLog(logMessages, "gzip"); } [Fact] @@ -197,31 +188,13 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests [InlineData("text/plain2")] public async Task MimeTypes_OtherContentTypes_NoMatch(string contentType) { - var builder = new WebHostBuilder() - .ConfigureServices(services => - { - services.AddResponseCompression(); - }) - .Configure(app => - { - app.UseResponseCompression(); - app.Run(context => - { - context.Response.Headers[HeaderNames.ContentMD5] = "MD5"; - context.Response.ContentType = contentType; - return context.Response.WriteAsync(new string('a', 100)); - }); - }); - - var server = new TestServer(builder); - var client = server.CreateClient(); - - var request = new HttpRequestMessage(HttpMethod.Get, ""); - request.Headers.AcceptEncoding.ParseAdd("gzip"); - - var response = await client.SendAsync(request); + var (response, logMessages) = await InvokeMiddleware(uncompressedBodyLength: 100, requestAcceptEncodings: new[] { "gzip" }, contentType); CheckResponseNotCompressed(response, expectedBodyLength: 100, sendVaryHeader: false); + Assert.Equal(2, logMessages.Count); + AssertLog(logMessages.First(), LogLevel.Trace, "This request accepts compression."); + var expected = string.IsNullOrEmpty(contentType) ? "(null)" : contentType; + AssertLog(logMessages.Skip(1).First(), LogLevel.Debug, $"Response compression is not enabled for the Content-Type '{expected}'."); } [Theory] @@ -287,88 +260,42 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests public async Task MimeTypes_IncludedAndExcluded( string[] mimeTypes, string[] excludedMimeTypes, - string mimeType, + string contentType, bool compress ) { - var builder = new WebHostBuilder() - .ConfigureServices( - services => - services.AddResponseCompression( - options => - { - options.MimeTypes = mimeTypes; - options.ExcludedMimeTypes = excludedMimeTypes; - } - ) - ) - .Configure( - app => - { - app.UseResponseCompression(); - app.Run( - context => - { - context.Response.Headers[HeaderNames.ContentMD5] = "MD5"; - context.Response.ContentType = mimeType; - return context.Response.WriteAsync(new string('a', 100)); - } - ); - } - ); - - var server = new TestServer(builder); - var client = server.CreateClient(); - - var request = new HttpRequestMessage(HttpMethod.Get, ""); - request.Headers.AcceptEncoding.ParseAdd("gzip"); - - var response = await client.SendAsync(request); + var (response, logMessages) = await InvokeMiddleware(uncompressedBodyLength: 100, requestAcceptEncodings: new[] { "gzip" }, contentType, + configure: options => + { + options.MimeTypes = mimeTypes; + options.ExcludedMimeTypes = excludedMimeTypes; + }); if (compress) { CheckResponseCompressed(response, expectedBodyLength: 24, expectedEncoding: "gzip"); + AssertCompressedWithLog(logMessages, "gzip"); } else { CheckResponseNotCompressed(response, expectedBodyLength: 100, sendVaryHeader: false); + Assert.Equal(2, logMessages.Count); + AssertLog(logMessages.First(), LogLevel.Trace, "This request accepts compression."); + AssertLog(logMessages.Skip(1).First(), LogLevel.Debug, $"Response compression is not enabled for the Content-Type '{contentType}'."); } } [Fact] public async Task NoIncludedMimeTypes_UseDefaults() { - var builder = new WebHostBuilder() - .ConfigureServices( - services => - services.AddResponseCompression( - options => options.ExcludedMimeTypes = new[] { "text/*" } - ) - ) - .Configure( - app => - { - app.UseResponseCompression(); - app.Run( - context => - { - context.Response.Headers[HeaderNames.ContentMD5] = "MD5"; - context.Response.ContentType = TextPlain; - return context.Response.WriteAsync(new string('a', 100)); - } - ); - } - ); - - var server = new TestServer(builder); - var client = server.CreateClient(); - - var request = new HttpRequestMessage(HttpMethod.Get, ""); - request.Headers.AcceptEncoding.ParseAdd("gzip"); - - var response = await client.SendAsync(request); + var (response, logMessages) = await InvokeMiddleware(uncompressedBodyLength: 100, requestAcceptEncodings: new[] { "gzip" }, TextPlain, + configure: options => + { + options.ExcludedMimeTypes = new[] { "text/*" }; + }); CheckResponseCompressed(response, expectedBodyLength: 24, expectedEncoding: "gzip"); + AssertCompressedWithLog(logMessages, "gzip"); } [Theory] @@ -410,12 +337,14 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests [Fact] public async Task Request_AcceptStar_Compressed() { - var response = await InvokeMiddleware(100, requestAcceptEncodings: new[] { "*" }, responseType: TextPlain); + var (response, logMessages) = await InvokeMiddleware(100, requestAcceptEncodings: new[] { "*" }, responseType: TextPlain); #if NET461 CheckResponseCompressed(response, expectedBodyLength: 24, expectedEncoding: "gzip"); + AssertCompressedWithLog(logMessages, "gzip"); #elif NETCOREAPP2_2 CheckResponseCompressed(response, expectedBodyLength: 20, expectedEncoding: "br"); + AssertCompressedWithLog(logMessages, "br"); #else #error Target frameworks need to be updated. #endif @@ -424,9 +353,13 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests [Fact] public async Task Request_AcceptIdentity_NotCompressed() { - var response = await InvokeMiddleware(100, requestAcceptEncodings: new[] { "identity" }, responseType: TextPlain); + var (response, logMessages) = await InvokeMiddleware(100, requestAcceptEncodings: new[] { "identity" }, responseType: TextPlain); CheckResponseNotCompressed(response, expectedBodyLength: 100, sendVaryHeader: true); + Assert.Equal(3, logMessages.Count); + AssertLog(logMessages.First(), LogLevel.Trace, "This request accepts compression."); + AssertLog(logMessages.Skip(1).First(), LogLevel.Trace, "Response compression is available for this Content-Type."); + AssertLog(logMessages.Skip(2).First(), LogLevel.Debug, "No matching response compression provider found."); } [Theory] @@ -435,37 +368,48 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests [InlineData(new[] { "identity;q=0.5", "gzip" }, 24)] public async Task Request_AcceptWithHigherCompressionQuality_Compressed(string[] acceptEncodings, int expectedBodyLength) { - var response = await InvokeMiddleware(100, requestAcceptEncodings: acceptEncodings, responseType: TextPlain); + var (response, logMessages) = await InvokeMiddleware(100, requestAcceptEncodings: acceptEncodings, responseType: TextPlain); CheckResponseCompressed(response, expectedBodyLength: expectedBodyLength, expectedEncoding: "gzip"); + AssertCompressedWithLog(logMessages, "gzip"); } [Theory] [InlineData(new[] { "gzip;q=0.5", "identity;q=0.8" }, 100)] public async Task Request_AcceptWithhigherIdentityQuality_NotCompressed(string[] acceptEncodings, int expectedBodyLength) { - var response = await InvokeMiddleware(100, requestAcceptEncodings: acceptEncodings, responseType: TextPlain); + var (response, logMessages) = await InvokeMiddleware(100, requestAcceptEncodings: acceptEncodings, responseType: TextPlain); CheckResponseNotCompressed(response, expectedBodyLength: expectedBodyLength, sendVaryHeader: true); + Assert.Equal(3, logMessages.Count); + AssertLog(logMessages.First(), LogLevel.Trace, "This request accepts compression."); + AssertLog(logMessages.Skip(1).First(), LogLevel.Trace, "Response compression is available for this Content-Type."); + AssertLog(logMessages.Skip(2).First(), LogLevel.Debug, "No matching response compression provider found."); } [Fact] public async Task Response_UnknownMimeType_NotCompressed() { - var response = await InvokeMiddleware(100, requestAcceptEncodings: new[] { "gzip" }, responseType: "text/custom"); + var (response, logMessages) = await InvokeMiddleware(100, requestAcceptEncodings: new[] { "gzip" }, responseType: "text/custom"); CheckResponseNotCompressed(response, expectedBodyLength: 100, sendVaryHeader: false); + Assert.Equal(2, logMessages.Count); + AssertLog(logMessages.First(), LogLevel.Trace, "This request accepts compression."); + AssertLog(logMessages.Skip(1).First(), LogLevel.Debug, "Response compression is not enabled for the Content-Type 'text/custom'."); } [Fact] public async Task Response_WithContentRange_NotCompressed() { - var response = await InvokeMiddleware(50, requestAcceptEncodings: new[] { "gzip" }, responseType: TextPlain, addResponseAction: (r) => + var (response, logMessages) = await InvokeMiddleware(50, requestAcceptEncodings: new[] { "gzip" }, responseType: TextPlain, addResponseAction: (r) => { r.Headers[HeaderNames.ContentRange] = "1-2/*"; }); CheckResponseNotCompressed(response, expectedBodyLength: 50, sendVaryHeader: false); + Assert.Equal(2, logMessages.Count); + AssertLog(logMessages.First(), LogLevel.Trace, "This request accepts compression."); + AssertLog(logMessages.Skip(1).First(), LogLevel.Debug, "Response compression disabled due to the Content-Range header."); } @@ -474,7 +418,7 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests { var otherContentEncoding = "something"; - var response = await InvokeMiddleware(50, requestAcceptEncodings: new[] { "gzip" }, responseType: TextPlain, addResponseAction: (r) => + var (response, logMessages) = await InvokeMiddleware(50, requestAcceptEncodings: new[] { "gzip" }, responseType: TextPlain, addResponseAction: (r) => { r.Headers[HeaderNames.ContentEncoding] = otherContentEncoding; }); @@ -482,6 +426,9 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests Assert.True(response.Content.Headers.ContentEncoding.Contains(otherContentEncoding)); Assert.False(response.Content.Headers.ContentEncoding.Contains("gzip")); Assert.Equal(50, response.Content.Headers.ContentLength); + Assert.Equal(2, logMessages.Count); + AssertLog(logMessages.First(), LogLevel.Trace, "This request accepts compression."); + AssertLog(logMessages.Skip(1).First(), LogLevel.Debug, "Response compression disabled due to the Content-Encoding header."); } [Theory] @@ -489,9 +436,15 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests [InlineData(true, 24)] public async Task Request_Https_CompressedIfEnabled(bool enableHttps, int expectedLength) { + var sink = new TestSink( + TestSink.EnableWithTypeName, + TestSink.EnableWithTypeName); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); + var builder = new WebHostBuilder() .ConfigureServices(services => { + services.AddSingleton(loggerFactory); services.AddResponseCompression(options => { options.EnableForHttps = enableHttps; @@ -521,6 +474,16 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests var response = await client.SendAsync(request); Assert.Equal(expectedLength, response.Content.ReadAsByteArrayAsync().Result.Length); + + var logMessages = sink.Writes.ToList(); + if (enableHttps) + { + AssertCompressedWithLog(logMessages, "gzip"); + } + else + { + AssertLog(logMessages.Single(), LogLevel.Debug, "No response compression available for HTTPS requests. See ResponseCompressionOptions.EnableForHttps."); + } } [Theory] @@ -985,17 +948,23 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests Assert.False(fakeSendFile.Invoked); } - private Task InvokeMiddleware( + private async Task<(HttpResponseMessage, List)> InvokeMiddleware( int uncompressedBodyLength, string[] requestAcceptEncodings, string responseType, Action addResponseAction = null, Action configure = null) { + var sink = new TestSink( + TestSink.EnableWithTypeName, + TestSink.EnableWithTypeName); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); + var builder = new WebHostBuilder() .ConfigureServices(services => { services.AddResponseCompression(configure ?? (_ => { })); + services.AddSingleton(loggerFactory); }) .Configure(app => { @@ -1019,7 +988,7 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests request.Headers.AcceptEncoding.Add(System.Net.Http.Headers.StringWithQualityHeaderValue.Parse(requestAcceptEncodings[i])); } - return client.SendAsync(request); + return (await client.SendAsync(request), sink.Writes.ToList()); } private void CheckResponseCompressed(HttpResponseMessage response, int expectedBodyLength, string expectedEncoding) @@ -1063,6 +1032,20 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests Assert.Equal(expectedBodyLength, response.Content.Headers.ContentLength); } + private void AssertLog(WriteContext log, LogLevel level, string message) + { + Assert.Equal(level, log.LogLevel); + Assert.Equal(message, log.State.ToString()); + } + + private void AssertCompressedWithLog(List logMessages, string provider) + { + Assert.Equal(3, logMessages.Count); + AssertLog(logMessages.First(), LogLevel.Trace, "This request accepts compression."); + AssertLog(logMessages.Skip(1).First(), LogLevel.Trace, "Response compression is available for this Content-Type."); + AssertLog(logMessages.Skip(2).First(), LogLevel.Debug, $"The response will be compressed with '{provider}'."); + } + private class FakeSendFileFeature : IHttpSendFileFeature { private readonly Stream _innerBody;