DelegatingHandler to decompress incoming requests in WebApi

asp.net-web-api, c#, compression

Solution

I resolved this.

It was an error in my HTTPClient implementation where I had moved from using PostAsJsonAsync to PostAsync to do the compression client side, but had not added the Content-Type header to specify application/json.

After doing that in the client, all is working as planned.

Problem

In my app we are sending decent size packets up from a client and receiving a decent sized response, so I want to implement some compression on the way up and the way back. On the way back it is fine because I can lean on IIS's dynamic compression to do that for me, but on the way up I am finding the following issue. I have a delegating handler that sits there to decompress the incoming requests: (Most of this code is based on parts of Fabrik.Common (https://github.com/benfoster/Fabrik.Common)) ``` public class DecompressionHandler : DelegatingHandler { public Collection<ICompressor> Compressors; public DecompressionHandler() { Compressors = new Collection<ICompressor> {new GZipCompressor(), new DeflateCompressor()}; } protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { if (request.Content.Headers.ContentEncoding.IsntNullOrEmpty() && request.Content != null) { var encoding = request.Content.Headers.ContentEncoding.First(); var compressor = Compressors.FirstOrDefault(c => c.EncodingType.Equals(encoding, StringComparison.InvariantCultureIgnoreCase)); if (compressor != null) { request.Content = await DecompressContentAsync(request.Content, compressor).ConfigureAwait(true); } } var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(true); return response; } private static async Task<HttpContent> DecompressContentAsync(HttpContent compressedContent, ICompressor compressor) { using (compressedContent) { var decompressed = new MemoryStream(); await compressor.Decompress(await compressedContent.ReadAsStreamAsync(), decompressed).ConfigureAwait(true); // set position back to 0 so it can be read again decompressed.Position = 0; var newContent = new StreamContent(decompressed); // copy content type so we know how to load correct formatter newContent.Headers.ContentType = compressedContent.Headers.ContentType; return newContent; } } } public class DeflateCompressor : Compressor { private const string DeflateEncoding = "deflate"; public override string EncodingType { get { return DeflateEncoding; } } public override Stream CreateCompressionStream(Stream output) { return new DeflateStream(output, CompressionMode.Compress, leaveOpen: true); } public override Stream CreateDecompressionStream(Stream input) { return new DeflateStream(input, CompressionMode.Decompress, leaveOpen: true); } } public abstract class Compressor : ICompressor { public abstract string EncodingType { get; } public abstract Stream CreateCompressionStream(Stream output); public abstract Stream CreateDecompressionStream(Stream input); public virtual Task Compress(Stream source, Stream destination) { var compressed = CreateCompressionStream(destination); return Pump(source, compressed) .ContinueWith(task => compressed.Dispose()); } public virtual Task Decompress(Stream source, Stream destination) { var decompressed = CreateDecompressionStream(source); return Pump(decompressed, destination) .ContinueWith(task => decompressed.Dispose()); } protected virtual Task Pump(Stream input, Stream output) { return input.CopyToAsync(output); } } public interface ICompressor { string EncodingType { get; } Task Compress(Stream source, Stream destination); Task Decompress(Stream source, Stream destination); } public class GZipCompressor : Compressor { private const string GZipEncoding = "gzip"; public override string EncodingType { get { return GZipEncoding; } } public override Stream CreateCompressionStream(Stream output) { return new GZipStream(output, CompressionMode.Compress, leaveOpen: true); } public override Stream CreateDecompressionStream(Stream input) { return new GZipStream(input, CompressionMode.Decompress, leaveOpen: true); } } ``` The decompression works fine and I have my request.Content populated with a result that is my decompressed JSON. When I pass that off to base.SendAsync and it hits my controller method, the model is null, whereas before I implemented compression it was all working great. I have read that when you Read the content stream coming in that it is a once off thing, but I thought setting the request.content to the decompressed result should let it be read again?

Original source