Compress HTTP GET Response
Asked Answered
S

3

17

I am currently working on migrating few of my MVC3 Controllers to MVC4 Api Controllers. I have implemented Compression mechanism for MVC3 controller Get Method Responses by inherting ActionFilterAttribute and overriding OnActionExecutiong method. After some Research I found that I need to use ActionFilterMethod from System.Web.HttpFilters. It would be great if somebody can share piece of sample code to get me started for this compressing HTTP response using GZip

Surgy answered 4/5, 2012 at 6:12 Comment(2)
I'm having the same problem, although in my case I already enabled IIS compression. In your case, was it the IIS compression, or did you create the custom handler?Floating
Yes, I have used custom handler for this just like the way Darin mentioned here.Surgy
I
40

The easiest is to enable compression directly at IIS level.

If you want to do it at the application level you could write a custom delegating message handler as shown in the following post:

public class CompressHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return base.SendAsync(request, cancellationToken).ContinueWith<HttpResponseMessage>((responseToCompleteTask) =>
        {
            HttpResponseMessage response = responseToCompleteTask.Result;

            if (response.RequestMessage.Headers.AcceptEncoding != null)
            {
                string encodingType = response.RequestMessage.Headers.AcceptEncoding.First().Value;

                response.Content = new CompressedContent(response.Content, encodingType);
            }

            return response;
        },
        TaskContinuationOptions.OnlyOnRanToCompletion);
    }
}

public class CompressedContent : HttpContent
{
    private HttpContent originalContent;
    private string encodingType;

    public CompressedContent(HttpContent content, string encodingType)
    {
        if (content == null)
        {
            throw new ArgumentNullException("content");
        }

        if (encodingType == null)
        {
            throw new ArgumentNullException("encodingType");
        }

        originalContent = content;
        this.encodingType = encodingType.ToLowerInvariant();

        if (this.encodingType != "gzip" && this.encodingType != "deflate")
        {
            throw new InvalidOperationException(string.Format("Encoding '{0}' is not supported. Only supports gzip or deflate encoding.", this.encodingType));
        }

        // copy the headers from the original content
        foreach (KeyValuePair<string, IEnumerable<string>> header in originalContent.Headers)
        {
            this.Headers.AddWithoutValidation(header.Key, header.Value);
        }

        this.Headers.ContentEncoding.Add(encodingType);
    }

    protected override bool TryComputeLength(out long length)
    {
        length = -1;

        return false;
    }

    protected override Task SerializeToStreamAsync(Stream stream, TransportContext context)
    {
        Stream compressedStream = null;

        if (encodingType == "gzip")
        {
            compressedStream = new GZipStream(stream, CompressionMode.Compress, leaveOpen: true);
        }
        else if (encodingType == "deflate")
        {
            compressedStream = new DeflateStream(stream, CompressionMode.Compress, leaveOpen: true);
        }

        return originalContent.CopyToAsync(compressedStream).ContinueWith(tsk =>
        {
            if (compressedStream != null)
            {
                compressedStream.Dispose();
            }
        });
    }
}

All that's left now is to register the handler in Application_Start:

GlobalConfiguration.Configuration.MessageHandlers.Add(new CompressHandler());
Indetermination answered 4/5, 2012 at 9:21 Comment(5)
I think there's a bug in this code (as well as in similar examples found on the web): The Content-Length Header is set incorrectly because the Content-Length Header is copied from the gzipped content. This can be easily reproduced by passing a StringContent through the Compression Handler. To fix this, the line with originalContent.Headers needs to be fixed like this: originalContent.Headers.Where(x => x.Key != "Content-Length")Dilettantism
Code will fail if no Accept-Encoding is provided. if (response.RequestMessage.Headers.AcceptEncoding != null) should be if (response.RequestMessage.Headers.AcceptEncoding.Any())Scavenger
I'd recommend adding the following in SendAsync between the assignment of encodingType and assignment of response.Content to allow error responses to return without compression if (response.StatusCode != HttpStatusCode.OK || response.Content == null || string.IsNullOrWhiteSpace(encodingType)) return response;Phrygian
I needed to replace the AcceptEncoding check with the following code: if (response.RequestMessage.Headers.AcceptEncoding.Any()) { string encodingType = response.RequestMessage.Headers.AcceptEncoding.First().Value; if (response.Content != null) { response.Content = new CompressedContent(response.Content, encodingType); } }Chiffon
How would you incorporate response.Content.LoadIntoBufferAsync() to get the length of the response content (response.Content.Headers.ContentLength) and then exclude the result from zipping if it is smaller then some threshold? When the line above is added before setting response.Content, the call ends in an timeout / deadlockBellerophon
I
6

If you are using IIS 7+, I would say leave the compression to IIS as it supports GZIP compression. Just turn it on.

On the other hand, compression is too close to the metal for the controller. Ideally controller should work in much higher level than bytes and streams.

Improvisatory answered 4/5, 2012 at 8:40 Comment(1)
In general I agree, however IIS level compression would require configuration of any servers using it.Grandmother
H
3

Use a class and write the following code

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CompressFilter : ActionFilterAttribute
{
    public override void OnActionExecuted(HttpActionExecutedContext context)
    {
        var acceptedEncoding = context.Response.RequestMessage.Headers.AcceptEncoding.First().Value;
        if (!acceptedEncoding.Equals("gzip", StringComparison.InvariantCultureIgnoreCase)
        && !acceptedEncoding.Equals("deflate", StringComparison.InvariantCultureIgnoreCase))
        {
            return;
        }
        context.Response.Content = new CompressedContent(context.Response.Content, acceptedEncoding);
    }
}

Now create another class and write the following code.

public class CompressedContent : HttpContent
{
    private readonly string _encodingType;
    private readonly HttpContent _originalContent;
    public CompressedContent(HttpContent content, string encodingType = "gzip")
    {
        if (content == null)
        {
            throw new ArgumentNullException("content");
        }
        _originalContent = content;
        _encodingType = encodingType.ToLowerInvariant();
        foreach (var header in _originalContent.Headers)
        {
            Headers.TryAddWithoutValidation(header.Key, header.Value);
        }
        Headers.ContentEncoding.Add(encodingType);
    }
    protected override bool TryComputeLength(out long length)
    {
        length = -1;
        return false;
    }
    protected override Task SerializeToStreamAsync(Stream stream, TransportContext context)
    {
        Stream compressedStream = null;
        switch (_encodingType)
        {
            case "gzip":
                compressedStream = new GZipStream(stream, CompressionMode.Compress, true);
                break;
            case "deflate":
                compressedStream = new DeflateStream(stream, CompressionMode.Compress, true);
                break;
            default:
                compressedStream = stream;
                break;
        }
        return _originalContent.CopyToAsync(compressedStream).ContinueWith(tsk =>
        {
            if (compressedStream != null)
            {
                compressedStream.Dispose();
            }
        });
    }
}

Now use the following attribute in Controller or in any api action method like this

[Route("GetData")]
[CompressFilter]         
public HttpResponseMessage GetData()
{
}
Hinch answered 9/8, 2016 at 13:5 Comment(4)
I have OWIN Middleware configured on my Web API and this is the only solution that worked for me. Plus, you can really target what you want to compress. Good solution!Carpenter
This will fail if you do "return (Ok());" in your controller method, because _originalContent will be null and you will get an exception about "The async operation did not return a System.Threading.Tasks.Task object."... how do I fix it?Swatter
Ah, just add this in OnActionExecuted(): "if (context.Response.Content == null) return;"Swatter
It will also fail (crash) if there is no Accept-Encoding header in the request. That First() method. Do this instead: string acceptedEncoding = string.Empty; var acceptedEncodingHeaders = context.Response.RequestMessage.Headers.AcceptEncoding; if (acceptedEncodingHeaders.Any()) acceptedEncoding = acceptedEncodingHeaders.First().Value;Swatter

© 2022 - 2024 — McMap. All rights reserved.