33

I have a WebAPI controller that returns an HttpResponseMessage and I want to add gzip compression. This is the server code:

using System.Net.Http;
using System.Web.Http;
using System.Web;
using System.IO.Compression;

[Route("SomeRoute")]
public HttpResponseMessage Post([FromBody] string value)
{
    HttpContext context = HttpContext.Current;

    context.Response.Filter = new GZipStream(context.Response.Filter, CompressionMode.Compress);

    HttpContext.Current.Response.AppendHeader("Content-encoding", "gzip");
    HttpContext.Current.Response.Cache.VaryByHeaders["Accept-encoding"] = true;

    return new SomeClass().SomeRequest(value);
}

And this is the client code for the ajax call, using jquery:

$.ajax({
    url: "/SomeRoute",
    type: "POST",
    cache: "false",
    data: SomeData,
    beforeSend: function (jqXHR) { jqXHR.setRequestHeader('Accept-Encoding', 'gzip'); },
    success: function(msg) { ... }

When I run this, the server code returns without bugging but the client bugs:

(failed)
net::ERR_CONTENT_DECODING_FAILED

enter image description here

When I look with Fiddler, this is what I see:

enter image description here

What do I need to change to make the web service return gzipped content that the client processes normally? I know I could also do this with an HttpModule or through some setting on IIS but neither option fits the scenario of the hosting:

enter image description here

Please note that I'm not looking for an IIS setting because I don't have access to that (hosting).

frenchie
  • 51,731
  • 109
  • 304
  • 510

4 Answers4

53

Add these NuGet packages:

Microsoft.AspNet.WebApi.Extensions.Compression.Server System.Net.Http.Extensions.Compression.Client

Then and add one line of code to App_Start\WebApiConfig.cs:

GlobalConfiguration.Configuration.MessageHandlers.Insert(0, new ServerCompressionHandler(new GZipCompressor(), new DeflateCompressor()));

That will do the trick!

Details at:

**Updated after comment from @JCisar

Update for ASP.Net Core

Nuget Package is

Microsoft.AspNetCore.ResponseCompression

starball
  • 20,030
  • 7
  • 43
  • 238
Pooran
  • 1,640
  • 2
  • 18
  • 25
  • 4
    This is so much better than turning on dynamic IIS compression – Brain2000 Jun 30 '15 at 19:02
  • 7
    Thanks! Although, the package does say it is obsolete (I am guessing as of 01-28-2016). Anyone know if this is replaced by something that I should be using instead? I don't see any mention of it. – JCisar Feb 29 '16 at 18:05
  • 4
    @JCisar according to the package author, you're supposed to be using [those new packages](https://github.com/azzlack/Microsoft.AspNet.WebApi.MessageHandlers.Compression) that are being maintained by him. – Zignd Apr 03 '16 at 13:46
  • 1
    Note I could not get this to work via this method and in fact had to use `config.MessageHandlers.Insert(0, new ServerCompressionHandler(new GZipCompressor(), new DeflateCompressor()));` within the `Register` method – Chris Sep 12 '16 at 09:34
  • This is an obsolete package, it only installs the Microsoft.AspNet.WebApi.Extensions.Compression.Server and System.Net.Http.Extensions.Compression.Client packages. – Thulani Chivandikwa May 24 '17 at 11:55
  • @JCisar Updated .. thanks for pointing out. Updated the links. – Pooran May 30 '17 at 19:01
  • how to make it work, only if user requested to do so? – Hassan Faghihi Jul 13 '17 at 07:44
  • I think it's worth noting that both of those packages are not official Microsoft packages, despite their misleading name. – Sam Rueby Sep 18 '19 at 18:08
26

If you have access to IIS configuration

You cant just apply the header and hope it will be gzipped - the response will not be zipped.

You need remove the header you added and ensure you have the dynamic compression and static content compression are enabled on your IIS server.

One of the commenter's mentioned a good resource link here at stakoverflow that show how to do that:

Enable IIS7 gzip

Note it will only work setting the value in web.config if dynamic compression is already installed (which is not in a default install of IIS)

You can find the information about this on MSDN documentation: http://www.iis.net/configreference/system.webserver/httpcompression

Simple compression

Below is using a simple example of doing your own compression this example is using the Web Api MVC 4 project from visual studio project templates. To get compression working for HttpResponseMessages you have to implement a custom MessageHandler. See below a working example.

See the code implementation below.

Please note that I tried to keep the method doing the same as your example.

using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;

namespace MvcApplication1.Controllers
{
    public class ValuesController : ApiController
    {
        public class Person
        {
            public string name { get; set; }
        }
        // GET api/values
        public IEnumerable<string> Get()
        {
            HttpContext.Current.Response.Cache.VaryByHeaders["accept-encoding"] = true;

            return new [] { "value1", "value2" };
        }

        // GET api/values/5
        public HttpResponseMessage Get(int id)
        {
            HttpContext.Current.Response.Cache.VaryByHeaders["accept-encoding"] = true;

            var TheHTTPResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK); 
            TheHTTPResponse.Content = new StringContent("{\"asdasdasdsadsad\": 123123123 }", Encoding.UTF8, "text/json"); 

            return TheHTTPResponse;
        }

        public class EncodingDelegateHandler : 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 &&
                        response.RequestMessage.Headers.AcceptEncoding.Count > 0)
                    {
                        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.TryAddWithoutValidation(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();
                    }
                });
            }
        }
    }
}

Also add the new message handler to the config of your app.

using System.Web.Http;
using MvcApplication1.Controllers;

namespace MvcApplication1
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

            config.MessageHandlers.Add(new ValuesController.EncodingDelegateHandler());

            config.EnableSystemDiagnosticsTracing();
        }
    }
}

The Custom handler was put together by - Kiran Challa (http://blogs.msdn.com/b/kiranchalla/archive/2012/09/04/handling-compression-accept-encoding-sample.aspx)

There are better examples that implement deflating of inbound streams too you can see examples of that below:

Additionally I found a really nice project that supports all of this on github.

Note while I arrived to this answer by myself Simon in your comments suggested this approach 2 days ago from the date of this answer.

Community
  • 1
  • 1
dmportella
  • 4,614
  • 1
  • 27
  • 44
  • 1
    I'm not looking for an IIS solution because I don't have access to that. That's why I need to do the compression out of WebAPI. – frenchie Jul 24 '14 at 14:31
  • 3
    Use the example on http://benfoster.io/blog/aspnet-web-api-compression that does it outside of iis. – dmportella Jul 24 '14 at 14:32
  • That doesn't work in a hosted enviro: – frenchie Jul 24 '14 at 14:35
  • if dynamic compression is not installed you wont be able to use it – dmportella Jul 24 '14 at 14:40
  • you have to implement your own using the classes on ben fosters post – dmportella Jul 24 '14 at 14:40
  • the sample on msdn you dont need to use the attribute on httpCompression or the scheme element. – dmportella Jul 24 '14 at 14:41
  • Everybody says "it's easy, take a look at this link". Your answer lots of links, but no code. – frenchie Jul 28 '14 at 23:51
  • I can provide you an working example... all you had to do was ask. :( – dmportella Jul 29 '14 at 05:42
  • Ok, code is always better. I have the web service setup as shown in the question and I guess I'm just looking for the 3-5 lines of code that do the compression of the HTTP Response. – frenchie Jul 29 '14 at 13:02
  • I will post something tonight – dmportella Jul 29 '14 at 16:43
  • I see that your screenshot shows it works but the code is not returning an HttpResponseMessage. The code won't work when you return a raw HttpResponseMessage, that's the problem I'm looking to solve. – frenchie Jul 30 '14 at 23:40
  • Remove all the GET requests and just leave the POST. Then, replace your code with this: HttpResponseMessage TheHTTPResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK); TheHTTPResponse.Content = new StringContent("{\"d\": " + "test" + "}", Encoding.UTF8, "text/json"); return TheHTTPResponse; Run it and it'll return a json string. Now the problem is to make it return a gziped HttpResponseMessage. And that's when it doesn't work. – frenchie Jul 30 '14 at 23:44
  • Yep i will check it today. Might to use the common fabrik stuff on this – dmportella Jul 31 '14 at 05:55
  • I have added a working example now, finally it is working with http response messages.. had to implement a custom message handler. – dmportella Jul 31 '14 at 11:05
  • I found some even better examples you can use check my answer now – dmportella Jul 31 '14 at 11:10
  • What comment are you talking about? – dmportella Jul 31 '14 at 17:35
  • I haven't stolen Any bodies comment – dmportella Jul 31 '14 at 17:36
  • I broke my back getting to this answer and i havent used anyone elses answers here all places i have taken stuff from have been properly attributed. – dmportella Jul 31 '14 at 17:40
  • Probably what happen here is that there is another question in so that has an answer similar. I didnt see any had i found one i would have attributed. No need to down vote. – dmportella Jul 31 '14 at 17:43
  • Oh now i can see your comment on the question i could attribute to your comment but i arrived at that port with out the help of your comment since i had missed it. Why didnt you post an answer... – dmportella Jul 31 '14 at 17:55
  • @SimonMourier I didnt see your comment.. infact I found a ton of example online that i even noted on my answer i could have put another example which I almost did, it just happen I used the Kiran challas one because it has decompression and compression. ok I didnt steal your comment I found it on my own accord. – dmportella Jul 31 '14 at 18:10
  • @SimonMourier but to be a fair I added an attribution to you at the bottom – dmportella Jul 31 '14 at 18:17
  • 1
    Thanks for this, suspect you mean accept-encoding rather than accept-enconding! – The Senator May 30 '16 at 16:11
  • @TheSenator yes indeed well spotted – dmportella Jun 24 '16 at 12:43
5

One Solution without editing any IIS Setting or Installing any Nuget package is to add a MessageHandler to your WEB API.

This will catch requests with the "AcceptEncoding" Header and compress them using the Build in System.IO.Compression libraries.

public class CompressHandler : DelegatingHandler
{
    private static CompressHandler _handler;
    private CompressHandler(){}
    public static CompressHandler GetSingleton()
    {
        if (_handler == null)
            _handler = new CompressHandler();
        return _handler;
    }
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return base.SendAsync(request, cancellationToken).ContinueWith<HttpResponseMessage>((responseToCompleteTask) =>
        {
            HttpResponseMessage response = responseToCompleteTask.Result;
            var acceptedEncoding =GetAcceptedEncoding(response);
            if(acceptedEncoding!=null)
                response.Content = new CompressedContent(response.Content, acceptedEncoding);

            return response;
        },
        TaskContinuationOptions.OnlyOnRanToCompletion);
    }
    private string GetAcceptedEncoding(HttpResponseMessage response)
    {
        string encodingType=null;
        if (response.RequestMessage.Headers.AcceptEncoding != null && response.RequestMessage.Headers.AcceptEncoding.Any())
        {
            encodingType = response.RequestMessage.Headers.AcceptEncoding.First().Value;
        }
        return encodingType;
    }


}

    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.TryAddWithoutValidation(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();
            }
        });
    }
}

And add this handler to your Global.asax.cs

GlobalConfiguration.Configuration.MessageHandlers.Insert(0, CompressHandler.GetSingleton());

Kudos to Ben Foster. ASP.NET Web API Compression

Anestis Kivranoglou
  • 7,728
  • 5
  • 44
  • 47
2

Just an addendum to enabling compression in IIS via the applicationHost.config file.

Use the IIS config manager to make the changes or notepad.exe to edit the file. I was using Notepad++ and even though the file was saving, it actually was not.

Something to do with 32/64bit environments, configs and the programs that edit them. Ruined my afternoon!!

jenson-button-event
  • 18,101
  • 11
  • 89
  • 155