16

I have a fairly bog standard .net MVC 4 Web API application.

 public class LogsController : ApiController
{

    public HttpResponseMessage PostLog(List<LogDto> logs)
    {
        if (logs != null && logs.Any())
        {
            var goodLogs = new List<Log>();
            var badLogs = new List<LogBad>();

            foreach (var logDto in logs)
            {
                if (logDto.IsValid())
                {
                    goodLogs.Add(logDto.ToLog());
                }
                else
                {
                    badLogs.Add(logDto.ToLogBad());
                }
            }

            if (goodLogs.Any())
            {
                _logsRepo.Save(goodLogs);
            }

            if(badLogs.Any())
            {
                _logsBadRepo.Save(badLogs);
            }


        }
        return new HttpResponseMessage(HttpStatusCode.OK);
    }
}

This all work fine, I have devices that are able to send me their logs and it works well. However now we are starting to have concerns about the size of the data being transferred, and we want to have a look at accepting post that have been compressed using GZIP?

How would I go about do this? Is it setting in IIS or could I user Action Filters?

EDIT 1

Following up from Filip's answer my thinking is that I need to intercept the processing of the request before it gets to my controller. If i can catch the request before the Web api framework attempts to parse the body of the request into my business object, which fails because the body of the request is still compressed. Then I can decompress the body of the request and then pass the request back into the processing chain, and hopefully the Web Api framework will be able to parse the (decompressed) body into my business objects.

It looks Like using the DelagatingHandler is the way to go. It allows me access to the request during the processing, but before my controller. So I tried the following?

 public class gZipHandler : DelegatingHandler
{

    protected override System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
    {
        string encodingType = request.Headers.AcceptEncoding.First().Value;

        request.Content = new DeCompressedContent(request.Content, encodingType);

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

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

    public DeCompressedContent(HttpContent content, string encodType)
    {
        originalContent = content;
        encodingType = encodType;
    }

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

        return false;
    }


    protected override Task<Stream> CreateContentReadStreamAsync()
    {
        return base.CreateContentReadStreamAsync();
    }

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

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

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



}

}

This seems to be working ok. The SendAsync method is being called before my controller and the constructor for the DecompressedContent is being called. However the SerializeToStreamAsync is never being called so I added the CreateContentReadStreamAsync to see if that's where the decompressing should be happening, but that's not being called either.

I fell like I am close to the solution, but just need a little bit extra to get it over the line.

kaliatech
  • 17,579
  • 5
  • 72
  • 84
David Kethel
  • 2,440
  • 9
  • 29
  • 49

4 Answers4

24

I had the same requirement to POST gzipped data to a .NET web api controller. I came up with this solution:

public class GZipToJsonHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
                                                           CancellationToken cancellationToken)
    {
        // Handle only if content type is 'application/gzip'
        if (request.Content.Headers.ContentType == null ||
            request.Content.Headers.ContentType.MediaType != "application/gzip")
        {
            return base.SendAsync(request, cancellationToken);
        }

        // Read in the input stream, then decompress in to the outputstream.
        // Doing this asynronously, but not really required at this point
        // since we end up waiting on it right after this.
        Stream outputStream = new MemoryStream();
        Task task = request.Content.ReadAsStreamAsync().ContinueWith(t =>
            {
                Stream inputStream = t.Result;
                var gzipStream = new GZipStream(inputStream, CompressionMode.Decompress);

                gzipStream.CopyTo(outputStream);
                gzipStream.Dispose();

                outputStream.Seek(0, SeekOrigin.Begin);
            });

        // Wait for inputstream and decompression to complete. Would be nice
        // to not block here and work async when ready instead, but I couldn't 
        // figure out how to do it in context of a DelegatingHandler.
        task.Wait();

        // This next section is the key...

        // Save the original content
        HttpContent origContent = request.Content;

        // Replace request content with the newly decompressed stream
        request.Content = new StreamContent(outputStream);

        // Copy all headers from original content in to new one
        foreach (var header in origContent.Headers)
        {
            request.Content.Headers.Add(header.Key, header.Value);
        }

        // Replace the original content-type with content type
        // of decompressed data. In our case, we can assume application/json. A
        // more generic and reuseable handler would need some other 
        // way to differentiate the decompressed content type.
        request.Content.Headers.Remove("Content-Type");
        request.Content.Headers.Add("Content-Type", "application/json");

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

Using this approach, the existing controller, which normally works with JSON content and automatic model binding, continued to work without any changes.

I'm not sure why the other answer was accepted. It provides a solution for handling the responses (which is common), but not requests (which is uncommon). The Accept-Encoding header is used to specify acceptable response encodings, and is not related to request encodings.

kaliatech
  • 17,579
  • 5
  • 72
  • 84
  • Is it recommended to call Dispose on outputStream ? If yes, when ? Thanks – Amir Yonatan Mar 03 '15 at 12:04
  • Because outputStream is a MemoryStream, it is not required to dispose or close the stream explicitly. For details, see this answer: http://stackoverflow.com/questions/4274590/memorystream-close-or-memorystream-dispose – kaliatech Mar 03 '15 at 17:23
  • 5
    You should use Content-Encoding to decide if unzipping is necessary, not a custom application/gzip. The Content-Type should be left untouched. – Softlion Jun 08 '15 at 14:36
  • @Softlion Great suggestion. I'm tempted to modify the answer to show that, but I'm not in a position to verify that it would all still work as expected. If anyone can verify, feel free to modify the answer. Really though, the answer given by phasselbach is better than my original answer and it correctly uses Content-Encoding. – kaliatech Jun 08 '15 at 15:25
  • phasselbach answer is the correct one. Well DelegatingHandler is now deprecated, but it works well with Owin. – Softlion Jun 10 '15 at 06:52
  • @Softlion looking at https://learn.microsoft.com/en-us/dotnet/api/system.net.http.delegatinghandler?redirectedfrom=MSDN&view=netframework-4.7.2 I do not see any indications that DelegatingHandler would be deprecated. Nor does googling seem to indicate any such thing. Could you elaborate on the status of using a DelegatingHandler subclass for solving this problem? – Otto G Oct 01 '18 at 16:19
21

I believe the correct answer is Kaliatech's and I would have left this as a comment and voted his up is I had enough reputation points, since I think his is basically correct.

However, my situation called for the need to look at the encoding type type rather than the content type. Using this approach the calling system can still specify that the content type is json/xml/etc in the content type, but specify that the data is encoded using gzip or potentially another encoding/compression mechanism. This prevented me from needing to change the content type after decoding the input and allows any content type information to flow through in its original state.

Here's the code. Again, 99% of this is Kaliatech's answer including the comments, so please vote his post up if this is useful.

public class CompressedRequestHandler : DelegatingHandler
{
    protected override System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
    {
        if (IsRequetCompressed(request))
        {
            request.Content = DecompressRequestContent(request);
        }

        return base.SendAsync(request, cancellationToken);
    }

    private bool IsRequetCompressed(HttpRequestMessage request)
    {
        if (request.Content.Headers.ContentEncoding != null &&
            request.Content.Headers.ContentEncoding.Contains("gzip"))
        {
            return true;
        }

        return false;
    }

    private HttpContent DecompressRequestContent(HttpRequestMessage request)
    {
        // Read in the input stream, then decompress in to the outputstream.
        // Doing this asynronously, but not really required at this point
        // since we end up waiting on it right after this.
        Stream outputStream = new MemoryStream();
        Task task = request.Content.ReadAsStreamAsync().ContinueWith(t =>
            {
                Stream inputStream = t.Result;
                var gzipStream = new GZipStream(inputStream, CompressionMode.Decompress);

                gzipStream.CopyTo(outputStream);
                gzipStream.Dispose();

                outputStream.Seek(0, SeekOrigin.Begin);
            });

        // Wait for inputstream and decompression to complete. Would be nice
        // to not block here and work async when ready instead, but I couldn't 
        // figure out how to do it in context of a DelegatingHandler.
        task.Wait();

        // Save the original content
        HttpContent origContent = request.Content;

        // Replace request content with the newly decompressed stream
        HttpContent newContent = new StreamContent(outputStream);

        // Copy all headers from original content in to new one
        foreach (var header in origContent.Headers)
        {
            newContent.Headers.Add(header.Key, header.Value);
        }

        return newContent;
    }

I then registered this handler globally, which could be a dicey proposition if you are vulnerable to DoS attacks, but our service is locked down, so it works for us

GlobalConfiguration.Configuration.MessageHandlers.Add(new CompressedRequestHandler());
phasselbach
  • 211
  • 2
  • 2
  • 1
    I would be very interested if you explain why this would expose a DoS Attack vulnerability , is it because any request will raise a considerable CPU Usage? – Anestis Kivranoglou Nov 29 '16 at 11:41
6

While Web API doesn't support Accept-Encoding header out of the box, but Kiran has a terrific blog post on how to do that - http://blogs.msdn.com/b/kiranchalla/archive/2012/09/04/handling-compression-accept-encoding-sample.aspx - using a custom MessageHandler

If you implement his solution, all you need to do is issue a request with Accept-Encoding: gzip or Accept-Encoding: deflate header and the Web API response will be compressed in the message handler for you.

Filip W
  • 27,097
  • 6
  • 95
  • 82
  • 2
    Cool, thanks for that. Its was helpful. My scenario is a little bit different. I am not (yet) concerned with compressing the response from my web api, but rather accepting request (post) that have already been compressed. I need to decompress the body of these post and then handle the data in them. See edit in question for my next steps. Thanks – David Kethel Sep 11 '12 at 00:00
  • In the mean time there's actually an open source library created built on that post you mention and other blog posts on the subject: https://github.com/azzlack/Microsoft.AspNet.WebApi.MessageHandlers.Compression – fretje Jun 22 '16 at 14:22
0

try this

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

    /// <summary>
    /// 
    /// </summary>
    /// <param name="content"></param>
    /// <param name="encodingType"></param>
    public DeCompressedContent(HttpContent content, string encodingType)
    {

        if (content == null) throw new ArgumentNullException("content");
        if (string.IsNullOrWhiteSpace(encodingType)) throw new ArgumentNullException("encodingType");

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

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

        foreach (KeyValuePair<string, IEnumerable<string>> header in originalContent.Headers)
        {
            this.Headers.TryAddWithoutValidation(header.Key, header.Value);
        }

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

    /// <summary>
    /// 
    /// </summary>
    /// <param name="stream"></param>
    /// <param name="context"></param>
    /// <returns></returns>
    protected override Task SerializeToStreamAsync(Stream stream, TransportContext context)
    {
        var output = new MemoryStream();

        return this.originalContent
            .CopyToAsync(output).ContinueWith(task =>
            {
                // go to start
                output.Seek(0, SeekOrigin.Begin);

                if (this.encodingType.Equals("gzip", StringComparison.CurrentCultureIgnoreCase))
                {
                    using (var dec = new GZipStream(output, CompressionMode.Decompress))
                    {
                        dec.CopyTo(stream);
                    }
                }
                else
                {
                    using (var def = new DeflateStream(output, CompressionMode.Decompress))
                    {
                        def.CopyTo(stream);
                    }
                }

                if (output != null)
                    output.Dispose();
            });


    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="length"></param>
    /// <returns></returns>
    protected override bool TryComputeLength(out long length)
    {
        length = -1;

        return (false);
    }
}