Mike Mertsock
Mike Mertsock

Reputation: 12025

Supporting compressed request body with ServiceStack

I need to implement an endpoint that can accept a POST message with a gzip-compressed request body (not, not a compressed response body).

I found a way to handle this pretty easily by marking the request DTO with IRequiresRequestStream, and using GZipStream in the service class to decompress the raw request body and construct a string representing the decompressed data. It's a plain text request body, so in this case there's nothing lost by skipping automatic request DTO deserialization.

This is fine, I could stop there, but I was thinking about whether I could write this in a generic way so that every request posted to ServiceStack could be decompressed if it has the Content-Encoding header set correctly. I wrote a PreRequestFilter that was looking pretty good, until I ran into a problem: I can't find a way to modify the original request input stream or otherwise ensure that deserialization and other code later in the pipeline can access the decompressed bytes instead of the compressed data:

public static void Decompress(IRequest request, IResponse response)
{
    if (!request.Headers[HttpHeaders.ContentEncoding].EqualsIgnoreCase(CompressionTypes.GZip))
        return;

    request.UseBufferedStream = true;
    using (var decompressor = new GZipStream(request.InputStream, CompressionMode.Decompress))
    {
        var decompressedBytes = decompressor.ReadFully();

        // Fails because the MemoryStream, used when UseBufferedStream
        // is true, is not expandable.
        // Or, if we set request.UseBufferedStream = false first, then 
        // it fails because the original HttpInputStream is not writeable.
        request.InputStream.SetLength(decompressedBytes.Length);
        request.InputStream.Write(decompressedBytes, 0, decompressedBytes.Length);
   }
}
PreRequestFilters.Add(Decompress);

Is there any way to modify the request body in a PreRequestFilter or RawHttpHandler? Or alternatively, is there a totally different way to implement generic request body decompression for any content type/DTO? The per-request implementation I have working now is fine but it would be interesting to find a general solution.

Upvotes: 3

Views: 758

Answers (1)

mythz
mythz

Reputation: 143359

You can override AppHost.OnPreExecuteServiceFilter() to change what Request DTO is used to execute your Services with, e.g:

class AppHost : AppHostBase
{
    public virtual object OnPreExecuteServiceFilter(IService service,
        object request, IRequest httpReq, IResponse httpRes)
    {
        if (httpReq.Headers[HttpHeaders.ContentEncoding]
            .EqualsIgnoreCase(CompressionTypes.GZip))
        {
            //...
            return customRequest;
        }

        return request;
    }
}

Using a generic ASP.NET HTTP Module

Since this is something that can be genericized at the outer HTTP Protocol layer, an even better option would be to use generic a GZip HttpModule, e.g:

public class GZipRequestDecompressingModule : IHttpModule
{
    public void Init(HttpApplication context)
    {
        context.BeginRequest += (sender, e) =>
        {
            var request = (sender as HttpApplication).Request;

            string contentEncoding = request.Headers["Content-Encoding"];

            if (string.Equals(contentEncoding, "gzip",
                StringComparison.OrdinalIgnoreCase))
            {
                request.Filter = new GZipStream(request.Filter,
                    CompressionMode.Decompress);
                request.Headers.Remove("Content-Encoding");
            }
        };
    }
    public void Dispose()
    {
    }
}

Which can be configured in your Web.Config with:

<system.webServer>
    <modules runAllManagedModulesForAllRequests="true">
        <add name="AnyUniqueName"
            type="YourNamespace.GZipRequestDecompressingModule, YourAssembly"
            preCondition="integratedMode" />
    </modules>
</system.webServer>

Upvotes: 3

Related Questions