Compress HTTP GET Response

18,194

Solution 1

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());

Solution 2

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.

Solution 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()
{
}
Share:
18,194

Related videos on Youtube

Pavan Josyula
Author by

Pavan Josyula

I am working as senior developer. My areas of interest include Web &amp; Mobile Application Development using .NET &amp;iOS technologies.

Updated on June 04, 2022

Comments

  • Pavan Josyula
    Pavan Josyula almost 2 years

    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

  • aaddesso
    aaddesso about 11 years
    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")
  • Jan Sommer
    Jan Sommer over 10 years
    Code will fail if no Accept-Encoding is provided. if (response.RequestMessage.Headers.AcceptEncoding != null) should be if (response.RequestMessage.Headers.AcceptEncoding.Any())
  • Paul
    Paul almost 10 years
    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;
  • mike gold
    mike gold over 9 years
    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); } }
  • Philipp
    Philipp about 8 years
    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 / deadlock
  • samus
    samus over 6 years
    In general I agree, however IIS level compression would require configuration of any servers using it.