ASP.NET Core, response compression, response buffering and subtle difference between .NET Framework and .NET Core

I've received a question from a user of my Server-Sent Events Middleware. In general the user was asking if it can be used together with Response Compression Middleware because he had hard time making it work. As there shouldn't be any technical limitation to such scenario I've decided to quickly test it in my simple demo.

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddResponseCompression(options =>
        {
            options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[]
            {
                "text/event-stream"
            });
        });

        services.AddServerSentEvents();
        ...
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseResponseCompression()
            .MapServerSentEvents("/see-heartbeat")
            ...;

        ...
    }
}

It worked without any issues.

Chrome Developer Tools Network Tab - Server-Sent Events with gzip

I've quickly written a response including my snippet. Unfortunately the user was doing exactly same thing and while for me it was working perfectly for him it seemed to be doing nothing (there was no error or any other side effect, just events not arriving to the client). We have exchanged couple emails until we have finally discovered a difference in our scenarios - my target framework was netcoreapp1.0 while his was net451. I've switched my project to net451 and I could observe the same behavior.

Difference between .NET Framework and .NET Core

I've started looking for the root cause. It was obviously somehow related to the response compression but I didn't had an idea what could that be. Until I've found below fragment inside of GzipCompressionProvider.

public bool SupportsFlush
{
    get
    {
#if NET451
        return false;
#elif NETSTANDARD1_3
        return true;
#else
        // Not implemented, compiler break
#endif
    }
}

This clearly shows that GZipStream (which is being internally used by GzipCompressionProvider) doesn't support flushing in .NET Framework (and my Server Sent Events implementation is flushing the events when they are completely written). This difference is not documented, in fact the documentation states that GZipStream.Flush has not functionality regardless of implementation. I was able to find this issue which shed some light on how GZipStream has started actually flushing. The bottom line is that when used over .NET Framework the Response Compression Middleware is also buffering the response.

Response Buffering in ASP.NET Core

In ASP.NET Core component which provides response buffering capabilities should implement IHttpBufferingFeature. The Response Compression Middleware does it through BodyWrapperStream in which it wraps the original body stream. This means that it should be possible to disable the buffering with following code.

private void DisableResponseBuffering(HttpContext context)
{
    IHttpBufferingFeature bufferingFeature = context.Features.Get<IHttpBufferingFeature>();
    if (bufferingFeature != null)
    {
        bufferingFeature.DisableResponseBuffering();
    }
}

I've added this to ServerSentEventsMiddleware.

public class ServerSentEventsMiddleware
{
    ...

    public async Task Invoke(HttpContext context)
    {
        if (context.Request.Headers[Constants.ACCEPT_HTTP_HEADER] == Constants.SSE_CONTENT_TYPE)
        {
            DisableResponseBuffering(context);

            ...
        }
        else
        {
            await _next(context);
        }
    }

    ...
}

After this change running the demo application with net451 as target framework resulted in events correctly reaching the client. The difference was that the response wasn't compressed.

Chrome Developer Tools Network Tab - Server-Sent Events no compression for .NET Framework

This is how Response Compression Middleware handles the DisableResponseBuffering method. If compression provider which is supposed to be used doesn't support flushing (the SupportsFlush property above) it disables the compression.

This is an interesting and worth to remember difference in behavior between .NET Framework and .NET Core.