Skip to content
This repository has been archived by the owner on Dec 20, 2018. It is now read-only.

Response Compression middleware #52

Closed
wants to merge 11 commits into from
Closed

Response Compression middleware #52

wants to merge 11 commits into from

Conversation

Yves57
Copy link
Contributor

@Yves57 Yves57 commented Aug 4, 2016

A first PR proposition about #34

Included:

  • GZIP compression.
  • Options to set compression level, content minimum size threshold, accepted MIME types, additional compression providers.

Pending:

  • Probably optimizations in the temporary MemoryStream management.
  • A default accepted MIME types list?
  • A default value for MinimumSize option?
  • Other compression algorithms?

@dnfclas
Copy link

dnfclas commented Aug 4, 2016

Hi @Yves57, I'm your friendly neighborhood .NET Foundation Pull Request Bot (You can call me DNFBOT). Thanks for your contribution!
You've already signed the contribution license agreement. Thanks!

The agreement was validated by .NET Foundation and real humans are currently evaluating your PR.

TTYL, DNFBOT;


using (var uncompressedStream = new MemoryStream())
{
context.Response.Body = uncompressedStream;
Copy link
Member

Choose a reason for hiding this comment

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

Fully buffering all responses is not viable. We'll need to wrap the Response.Body stream and do the compression check on the first write.

Copy link
Contributor Author

@Yves57 Yves57 Aug 5, 2016

Choose a reason for hiding this comment

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

Is there any special test to add, or I can assume that in any cases the MIME type is available when the first byte is written in the body Stream? In understand that the header is written before the body, but I want to be sure that there are not special cases.

- Do not buffer always the response body.
- Remove optional parameters before analyzing the MIME type
else
{
context.Response.Headers[HeaderNames.ContentEncoding] = compressionProvider.EncodingName;
context.Response.Headers[HeaderNames.ContentMD5] = StringValues.Empty; // Reset the MD5 because the content changed.
Copy link
Member

Choose a reason for hiding this comment

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

Headers.Remove...

using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using System;
Copy link

Choose a reason for hiding this comment

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

Please put System.* ahead of other namespaces.

<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
</PropertyGroup>
<PropertyGroup Label="Configuration">
<RootNamespace>C:\ws\github\BasicMiddleware\samples\RewriteSample\Properties\AssemblyInfo.csResponseCompressionSample</RootNamespace>
Copy link

Choose a reason for hiding this comment

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

Remove?

@davidfowl
Copy link
Member

davidfowl commented Aug 19, 2016

@Yves57 Can you get some basic performance data when this middleware is active and doing its job and when it's not since it always wraps the stream.

_level = level;
}

public string EncodingName
Copy link

Choose a reason for hiding this comment

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

nit: follow form of property can make the code more condense and readable

public string EncodingName { get; } = "gzip";

@blowdart
Copy link
Member

I'd like to see a flag, which is set by default, so compression doesn't happen if the request protocol is HTTPS to combat variations on Beast, Crime et al.

@Tratcher
Copy link
Member

@DamianEdwards @muratg Are we actually planning to accept/ship this as an official package or should this be moved to contrib?

@blowdart
Copy link
Member

If you're shipping it then I definitely want my flag :D

@DamianEdwards
Copy link
Member

I'm more than happy to accept this from a function/product point of view (modulo usual API, security, perf, engineering reviews, etc.)

@Yves57
Copy link
Contributor Author

Yves57 commented Aug 19, 2016

For the HTTPS flag, is EnableHttps a correct name for you?

@Yves57
Copy link
Contributor Author

Yves57 commented Aug 19, 2016

@davidfowl I have used the TestServer to do very basic performance tests:

  • Body length: 100 bytes (so compression time itself is very small, but of course there is still the compression initialization time)
  • 10000 requests
  • Test written in a unit test run in Release

The results on my computer:

  • No compression middleware: 965 ms
  • Compression active but not used: 1025 ms
  • Compression active and used : 1500 ms

I will push the perf test source code in my next PR (I can remove it later if you want).

@Tratcher
Copy link
Member

How does it perform with Kestrel?

@Yves57
Copy link
Contributor Author

Yves57 commented Aug 22, 2016

@Tratcher @davidfowl

Other test:

  • Using "go-wrk" the client side (with 1 or 4 connections during 10 seconds).
  • Using the PR sample application, except that the body length is a 100 byte constant string
  • first run result ignored (warmup)

On my computer (so all in local) with 1 connection:

  • No compression middleware: 379 requests/s
  • Compression active but not used: 379 requests/s
  • Compression active and used: 353 requests/s

On my computer (so all in local) with 4 connections:

  • No compression middleware: 499 requests/s
  • Compression active but not used: 498 requests/s
  • Compression active and used: 494 requests/s

So the stream wrapper seems to be more or less "free" in term of performance, even if I'm a little bit surprised by the small difference between compression/none with 4 connections.

- New 'EnableHttps' flag
- Small source code feedbacks
@RehanSaeed
Copy link

RehanSaeed commented Aug 25, 2016

I'm going to be building a Request Decompression middleware (I work for a company that sends large JSON logs from Android apps in rural Africa where GPRS internet connectivity is low and compression would make a massive difference). Request Decompression is essentially going to be a mirror image of Response Compression (Decompress, instead of Compress).

Is there any appetite for extending this PR or starting a new PR based on this one?

IMHO, I'm really surprised that Request Decompression is not a thing in .NET or IIS and even more surprised that I've never wondered why it was missing. No easy built in option for HttpClient either.

@RehanSaeed
Copy link

RehanSaeed commented Aug 25, 2016

MIME Types

The MIME types have no default value, I propose the following list which only includes standard MIME types where possible and no common MIME type synonyms like text/javascript.

var mimeTypes = new string[]
{
    // Plain Text
    "text/html",
    "text/plain",
    "text/css",
    "text/mathml",
    "application/rtf",
    // JSON
    "application/javascript",
    "application/json",
    "application/manifest+json",
    "application/x-web-app-manifest+json",
    "text/cache-manifest",
    // XML
    "application/atom+xml",
    "application/rss+xml",
    "application/xslt+xml",
    "application/xml",
    // Fonts
    "font/opentype",
    "font/otf",
    "font/truetype",
    "application/font-woff",
    "application/vnd.ms-fontobject",
    "application/x-font-ttf",
    // Images
    "image/svg+xml",
    "image/x-icon"
};

You'd probably also want to use the above list for Request Compression, although the option to be different should still be there.

Minimum Size Option

IIS and NGINX provide an options which lets you specify a minimum size before compression is applied. This option should be added to the ResponseCompressionOptions.

NGINX also provides other options which I don't think are as important but should be considered:

  1. The HTTP version to enable compression for. NGINX sets the default to 1.1 and not 1.0.
  2. An option to use Regex to disable GZIP based on the User-Agent. This is commonly set to "MSIE [1-6]\." which disables GZIP for IE 1 to 6.

@Yves57
Copy link
Contributor Author

Yves57 commented Aug 25, 2016

@davidfowl @Tratcher
I just made a memory profiling. Here are the results for 3368 requests.

response_compression_memory

I think the most important information in that DeflateStream allocates a significant amount of memory (GZipStream is in fact a wrapper around DeflateStream).

  • A managed buffer of 8192 bytes that is allocated each time.
  • Unmanaged memory buffers are allocated and destroyed each time in ZLib, while a native function deflateReset allows to reset ZLib without destroying buffers.

I think a good optimization would be to add a method GZipStream.Set(Stream newOutput) to reset an existing GZipStream without destroying buffers and assign a new output. With this method, we could have GZipStream instances in a pool, and use them on demand.

@dodyg
Copy link

dodyg commented Sep 11, 2016

I had to resort using the following MIME type statement instead of just application/json because that's what context.Response.ContentType returns.

app.UseResponseCompression(new string[]{ "application/json; charset=utf-8" });

@Yves57
Copy link
Contributor Author

Yves57 commented Sep 11, 2016

@dodyg Are you sure that you have the latest version of the PR? The extension method with only MIME types does not exist anymore. And I just tried your sample without any problem.

]
},
"dependencies": {
"Microsoft.AspNetCore.Http": "1.1.0-*",
Copy link
Member

Choose a reason for hiding this comment

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

Middleware shouldn't need to reference Http directly. What are you using from here that isn't in abstractions?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right, "Http.Abstractions" is enough. I fix it.

@dodyg
Copy link

dodyg commented Sep 12, 2016

@Yves57 ah my fault. It works now.

            var compressionOptions = new ResponseCompressionOptions
            {
                MimeTypes = new string[]{ "application/json" }
            };
            app.UseResponseCompression(compressionOptions);

@andrewlock
Copy link

Just for reference, compression via middleware is available currently in WebMarkupMin.AspNetCore1 - I haven't compared the implementations, but there may be something of interest pertinent to this PR?

@Tratcher Tratcher self-assigned this Sep 22, 2016
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNET_ENV": "Development"
Copy link
Member

Choose a reason for hiding this comment

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

ASPNETCORE_ENVIRONMENT

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will fix, but just for information "ResponseBufferingSample" has the same error 😄 .

Copy link
Member

Choose a reason for hiding this comment

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

Feel free to fix the ResponseBufferingSample too :-)

"web": {
"commandName": "web",
"environmentVariables": {
"Hosting:Environment": "Development"
Copy link
Member

Choose a reason for hiding this comment

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

ASPNETCORE_ENVIRONMENT

IsMimeTypeCompressable(_response.ContentType); // MIME type in the authorized list
}

private bool IsMimeTypeCompressable(string mimeType)
Copy link
Member

Choose a reason for hiding this comment

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

This should be extracted to a generic ShouldCompressResponse callback on Options for more customizable control. Then we'd have an implementation based on mime types.

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.ResponseCompression
Copy link
Member

Choose a reason for hiding this comment

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

IApplicationBuilder extensions go into the Microsoft.AspNetCore.Builder namespace

Copy link
Member

Choose a reason for hiding this comment

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

Sometimes we'll move the Options there too, but we're less consistent about that.

/// 'False' to enable compression only on HTTP request. Enable compression on HTTPS request
/// may lead to security problems.
/// </summary>
public bool EnableHttps { get; set; } = false;
Copy link
Member

Choose a reason for hiding this comment

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

@blowdart should this option even be available?

Copy link
Member

Choose a reason for hiding this comment

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

No it shouldn't. Remove.

],
"xmlDoc": true
},
"description": "ASP.NET Core basic middleware for HTTP Response compression.",
Copy link
Member

Choose a reason for hiding this comment

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

remove basic

// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyConfiguration("")]
Copy link
Member

Choose a reason for hiding this comment

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

Assert.Equal(expectedLength, httpContext.Response.Body.Length);
}

private async Task<HttpResponse> InvokeMiddleware(int uncompressedBodyLength, string requestAcceptEncoding, string responseType, Action<HttpResponse> addResponseAction = null)
Copy link
Member

Choose a reason for hiding this comment

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


public override Task FlushAsync(CancellationToken cancellationToken)
{
if (_compressionStream != null)
Copy link
Member

Choose a reason for hiding this comment

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

This needs to call OnWrite() if the user is just trying to flush the headers


public override void Flush()
{
if (_compressionStream != null)
Copy link
Member

Choose a reason for hiding this comment

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

OnWrite();

Yves57 added 2 commits September 25, 2016 15:32
- Replace MIME types filter by a more generic delegate
/// 'False' to enable compression only on HTTP requests. Enable compression on HTTPS requests
/// may lead to security problems.
/// </summary>
public bool EnableHttps { get; set; } = false;
Copy link
Member

Choose a reason for hiding this comment

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

We discussed this offline. It can stay, but will remain off by default. bing, google, msn, and wikipedia all enable this, so we can't outright prohibit it.

@Tratcher
Copy link
Member

@RehanSaeed there are a few different issues with request compression. A) There's no opportunity for the client and server to negotiate the compression protocol, the client needs to know in advance what the server supports. B) the server may not need to decompress the body for a given operation. E.g. for your compressed log files, what if the web server only wanted to save them to disk in their compressed format.

Let's not try to put these two behaviors into one middleware. However, there may be some re-usable components between the two.

You can check with @DamianEdwards, but I don't anticipate accepting a contribution for request decompression. https://github.com/aspnet-contrib may be a better place for it.

Copy link
Member

@Tratcher Tratcher left a comment

Choose a reason for hiding this comment

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

Thanks for the update, it looks pretty good. The options still need some refinements, but I'll merge it and take over from here.

@Tratcher Tratcher added this to the 1.1.0 milestone Sep 26, 2016
@Tratcher
Copy link
Member

Rebased, squashed, and merged.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

10 participants