HTTP Compression with NancyFX

Some simple code to get http compression enabled in Nancy

Add a call to your Bootstrapper

In your Bootstrapper add a call to the NancyCompressionExtenstion.RegisterCompressionCheck extension method.

public class BootstrapperEx : DefaultNancyBootstrapper
{
    protected override void InitialiseInternal(TinyIoCContainer container)
    {
        base.InitialiseInternal(container);

        this.RegisterCompressionCheck();
    }
}

How this plugs into the Nancy pipeline

RegisterCompressionCheck adds a callback to the end of the response pipeline. Allowing us to compress after the module has returned but before the response it streamed to the Http pipeline.

public static void RegisterCompressionCheck(this DefaultNancyBootstrapper bootstrapper)
{
    bootstrapper.AfterRequest.AddItemToEndOfPipeline(CheckForCompression);
}

What happens in CheckForCompression

First some preconditions are evaluated to see if we even want to compress

Check the Request contains "gzip" in AcceptEncoding

static bool RequestIsGzipCompatible(Request request)
{
    return request.Headers.AcceptEncoding.Any(x => x.Contains("gzip"));
}

Check the Response is HttpStatusCode.OK

There is no point processing responses with other status codes.

if (context.Response.StatusCode != HttpStatusCode.OK)
{
    return;
}

Check the Response is a compatible Mime Type

There is no point compressing most binary Mime Types. So focus on text Mime Types.

public static List<string> ValidMimes = new List<string>
                                {
                                    "text/css",
                                    "text/html",
                                    "text/plain",
                                    "application/xml",
                                    "application/json",
                                    "application/xaml+xml",
                                    "application/x-javascript"
                                };

static bool ResponseIsCompatibleMimeType(Response response)
{
    return ValidMimes.Any(x => x == response.ContentType);
}

* Note: * ValidMimes is public so you can place this in a common library and still configure the valid Mime Types per application. Or, alternatively, just include the whole class and hard code the mime types in each app.

Check if the Content Length is too small

For very small amounts of content there is no point compressing it.

static bool ContentLengthIsTooSmall(Response response)
{
    string contentLength;
    if (response.Headers.TryGetValue("Content-Length", out contentLength))
    {
        var length = long.Parse(contentLength);
        if (length < 4096)
        {
            return true;
        }
    }
    return false;
}

The code that does the compression

The compression code is actually fairly simple

static void CompressResponse(Response response)
{
    response.Headers["Content-Encoding"] = "gzip";

    var contents = response.Contents;
    response.Contents = responseStream =>;
                            {
                                using (var compression = new GZipStream(responseStream, CompressionMode.Compress))
                                {
                                    contents(compression);
                                }
                            };
}

Summary

Again Nancy provided simple and elegant extension points to add functionality. So have a play and let me know if you find any issues.

Sample Code

There is a full working sample here:

http://simonsexperiments.googlecode.com/hg/NancyCompression

Full Code Listing

public static class NancyCompressionExtenstion
{

    public static void RegisterCompressionCheck(this DefaultNancyBootstrapper bootstrapper)
    {
        bootstrapper.AfterRequest.AddItemToEndOfPipeline(CheckForCompression);
    }

    static void CheckForCompression(NancyContext context)
    {
        if (!RequestIsGzipCompatible(context.Request))
        {
            return;
        }

        if (context.Response.StatusCode != HttpStatusCode.OK)
        {
            return;
        }

        if (!ResponseIsCompatibleMimeType(context.Response))
        {
            return;
        }

        if (ContentLengthIsTooSmall(context.Response))
        {
            return;
        }

        CompressResponse(context.Response);
    }

    static void CompressResponse(Response response)
    {
        response.Headers["Content-Encoding"] = "gzip";

        var contents = response.Contents;
        response.Contents = responseStream =>;
                                {
                                    using (var compression = new GZipStream(responseStream, CompressionMode.Compress))
                                    {
                                        contents(compression);
                                    }
                                };
    }

    static bool ContentLengthIsTooSmall(Response response)
    {
        string contentLength;
        if (response.Headers.TryGetValue("Content-Length", out contentLength))
        {
            var length = long.Parse(contentLength);
            if (length < 4096)
            {
                return true;
            }
        }
        return false;
    }

    public static List<string> ValidMimes = new List<string>;
                                                {
                                                    "text/css",
                                                    "text/html",
                                                    "text/plain",
                                                    "application/xml",
                                                    "application/json",
                                                    "application/xaml+xml",
                                                    "application/x-javascript"
                                                };

    static bool ResponseIsCompatibleMimeType(Response response)
    {
        return ValidMimes.Any(x => x == response.ContentType);
    }

    static bool RequestIsGzipCompatible(Request request)
    {
        return request.Headers.AcceptEncoding.Any(x => x.Contains("gzip"));
    }
}
Posted by: Simon Cropp
Last revised: 24 Dec, 2011 05:20 AM History

Comments

No comments yet. Be the first!

No new comments are allowed on this post.