Message Handler Implementation

Topics: Web Api
Apr 29, 2011 at 2:10 AM
Edited Apr 29, 2011 at 10:42 AM

Hi,

Again, just for study/investigation purposes, I'm implementing a message handler, that verifies the response content, and if it's a StreamContent type, I'm applying the compress. What do you say about it? Is it a scenario for message handlers?

public class GZipCompressMessageChannel : DelegatingChannel
{
    public GZipCompressMessageChannel(HttpMessageChannel innerChannel)
        : base(innerChannel) { }

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (request.Headers.AcceptEncoding.Contains(new StringWithQualityHeaderValue("gzip")))
        {
            return base.SendAsync(request, cancellationToken).ContinueWith<HttpResponseMessage>(t =>
            {
                var httpResponse = t.Result;

                if (httpResponse.Content is StreamContent)
                {
                    MemoryStream output = new MemoryStream();
                    ((StreamContent)httpResponse.Content).CopyTo(new GZipStream(output, CompressionMode.Compress));
                    output.Position = 0;

                    httpResponse.Content = new StreamContent(output);
                    httpResponse.Content.Headers.ContentEncoding.Add("gzip");
                }

                return httpResponse;
            }, cancellationToken);
        }

        return base.SendAsync(request, cancellationToken);
    }
}

Apr 29, 2011 at 12:59 PM

Does your MessageHandler only compress operations that explicitly return StreamContent?  

I did a similar thing but using the OperationHandlers.  First I created a new CompressedContent class that wraps any type of HttpContent that is returned.

 

public class CompressedContent : HttpContent {
        private readonly HttpContent _Content;
        public CompressedContent(HttpContent content) {
            _Content = content;
            Headers.ContentEncoding.Add("gzip");
        }

        protected override void SerializeToStream(Stream stream, TransportContext context) {
            using (var compressedStream = new GZipStream(stream, CompressionMode.Compress, false)) {
                _Content.CopyTo(compressedStream);
            }
        }

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

        protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) {
            throw new NotImplementedException();
        }

    }

and then the OperationHandler looks something like:

public class CompressionHandler : HttpOperationHandler<HttpResponseMessage, HttpResponseMessage>
    {
        public CompressionHandler() :base("response")
        {
        }

        protected override object OnHandle(HttpResponseMessage response)
        {
            if (response.RequestMessage.Headers.AcceptEncoding.Contains(new StringWithQualityHeaderValue("gzip")))
            {
                response.Content = new CompressedContent(response.Content);
            }
            return response;
        }
    }

The only advantage I see of using OperationHandlers over MessageHandlers is there is no overhead of checking the AcceptEncoding header for operations that return content-types that you know you don't want to compress, like images.  Although, the benefit obviously is that you don't need to do any clever logic at startup time to decide which operations to attach the handler to.

Apr 29, 2011 at 1:39 PM

@DarrelMiller thanks for excellent example. I have used HttpOperationHandler prior to methods being called. This one seems to be execute after the operation is called. Is that correct. If that is correct, does that happen based on the signature?

Apr 29, 2011 at 5:16 PM

Yeah, this handler goes in the Response pipeline instead of the Request pipeline.  You set them up in almost the same way. 

Apr 29, 2011 at 5:28 PM

Hello DarrelMiller,

Thanks a lot for your reply and great example.

My intention with MessageHandler example, was to compress all response messages, regardless of the operation that is being called from client, and from service developer perspective, he doesn't needs worry about compression, because messages handlers will intercept all of them.

But as you saw, I had to deal with async operations just to compress the output. My objective was reached with message handlers, but I prefer your solution.

May 17, 2011 at 11:33 PM

It's a little unclear to me where to add this HttpOperationHandler.
I tried doing something like:

var config = (HttpHostConfiguration)HttpHostConfiguration.Create();
Action<Collection<HttpOperationHandler>> handlers = c => {
   new CompressionHandler();
};
config.AddResponseHandlers(handlers, (s, o) => true);

But it just doesn't work.  Am I going about this wrong?

Coordinator
May 17, 2011 at 11:39 PM

you have to add it by calling c.Add....

Glenn

May 18, 2011 at 2:20 AM

I'm an idiot..thanks

May 19, 2011 at 5:01 PM

Regarding the CompressedContent class, it seems that the content type isn't respected (or gets lost). If I put a break point at the "Headers.ContentEncoding.Add("gzip")" I can inspect the _Content.Headers.ContentType and I see that it is text/json (as it should be).  I put a break point at the _Content.CopyTo(compressedStream) the ContentType is application/xml.  Next, oddly enough, the response in fiddler shows "Content-Type: text/html".

The following produces the json I expect:

GET http://localhost:2626/Email/jack@example.net HTTP/1.1
Host: localhost:2626
User-Agent: Fiddler
ApiKey: hope
Content-type: text/json

The following produces gzipped xml:

GET http://localhost:2626/Email/jack@example.net HTTP/1.1
Host: localhost:2626
User-Agent: Fiddler
ApiKey: hope
Content-type: text/json
Accept-Encoding: gzip

The gzipped xml response headers are the following:

HTTP/1.1 200 OK
Server: ASP.NET Development Server/10.0.0.0
Date: Thu, 19 May 2011 16:57:08 GMT
Content-Encoding: gzip
Content-Length: 256
Cache-Control: private
Content-Type: text/html
Connection: Close

I event tried adding to the constructor:

Headers.ContentType = _Content.Headers.ContentType;
but that didn't do anything.

Has anyone experienced anything similar?

May 20, 2011 at 3:15 PM

Yeah, sorry about that.  I added code to update the Content type in the compressed content and it seems to work for me. See this test http://webapicontrib.codeplex.com/SourceControl/changeset/view/3239d66af5ac#source%2fWebApiContrib.OperationHandlers.Tests%2fCompressionHandlerTests.cs

May 20, 2011 at 6:16 PM

I still get erroneous results above.  Is anyone else experiencing any problems?

If I set a breakpoint at the "Headers.ContentType = _Content.Headers.ContentType" I see "text/json" which is correct.

If I set another breakpoint at " _Content.CopyTo(compressedStream);" I can see that "this._Content.Headers.ContentType = {application/xml; charset=utf-8}"

Does this work for everyone else?

May 20, 2011 at 6:19 PM

A little more poking around I noticed that after the code:

public CompressedContent(HttpContent content) {
      _Content = content;
      Headers.ContentEncoding.Add("gzip");
      Headers.ContentType = _Content.Headers.ContentType; 
}
The Headers.ContentType is actually null, like the assignment doesn't work.  Maybe the "application/xml" or "text/html" is default and that is why I'm seeing the results I do.

May 20, 2011 at 6:37 PM

What is the concrete type of the HttpContent that you are trying to compress?  Is it an ObjectContent?  What Formatters do you have added to your host?  Did you try running my test to see if that worked for you?

ObjectContent is a bit wierd at the moment.  If you try to serialize it, it replaces it's internal collection of formatters from those configured on the host.  If it can't find a formatter that matches your media type, it replaces the media type with the media type of the first formatter that claims it is capable of serializing your T from ObjectContent<T>..

May 20, 2011 at 8:24 PM

I've gotten rid of all my custom formatters to try to isolate the problem, and your tests do pass.

My resource returns a HttpResponseMessage<MyObject>, that I want returned as json.  If it's not compressed the object is returned just fine (json), if it is compressed then it's returned as gzipped xml.

 

May 20, 2011 at 9:34 PM

I reported some bugs to Glenn last week about ObjectContent, media types and formatters.  I'm not sure why you are getting bitten by this, but I'll try doing some more tests using an ObjectContent and see if I can repro it.

May 21, 2011 at 4:24 AM

I can repro the problem when calling the service via fiddler but I have not yet been able to create a unit test that reproduces it consistently.

Dec 12, 2011 at 7:17 PM

Did you ever get to the bottom of what was going on here?  I'm trying to set up compression using this sample but hit the same error.  It would be great if it worked, though I have a hacky workaround - by chance I might have worked out what's happening:

 + I stepped through the code to the line "if response.RequestMessage.Headers.AcceptEncoding.Contains(new StringWithQualityHeaderValue("gzip")))". At this point, response.Content.Headers.ContentType was null.

+ I then browsed the response properties in VS locals window, then when I looked at ContentType again it had suddenly becomes set with the right value!

From this, is it possible that the formatters have not been run at this point, and that browsing the object properties in the local window had caused them to get run (e.g. in order to work out the content length or something)

My hacky workaround is simply to set the ContentType to the first "Accept" Header, if there is one.  As I am specifying a format via the query string and using a DelegatingChannel to overwrite the supplied Accept header, this seems to work for me.