Conditional responses with NancyFX

Disclaimer: This is part of my first foray into NancyFX and tweaking HTTP caching at this level.

Some Background

The ETag specification is defined by the ETag response and the If-None-Match request headers.

The Last-Modified specification is defined by the Last-Modified response and the If-Modified-Since request headers.

Nancy does the response headers for us

So Nancy already handles the response headers in GenericFileResponse

this.Headers["ETag"] = fi.LastWriteTimeUtc.Ticks.ToString("x");
this.Headers["Last-Modified"] = fi.LastWriteTimeUtc.ToString("R")

* Note: * Nancy only set the headers for the various file based response extensions. So AsFile, AsJs, AsCss and AsImage. For other response types you will need to set your own headers.

However Nancy does not handle the checking of request headers and doing a conditional 304 "Not Modified". So how do we achieve this cache check?

Add a call to your Module

In your Module add a call to the NancyModuleExtenstions.RegisterCacheCheck extension method (shown next).

public class MainModule:NancyModule
{
    public MainModule()
    {
        Get["/{FileName}"] = o => { return Response.AsFile((string)o.FileName +  ".txt"); };
        this.RegisterCacheCheck();
    }
}

Adding a "Not Modified" check

So here is the code that actually does the check.

public static class NancyModuleExtenstions
{

    public static void RegisterCacheCheck(this NancyModule nancyModule)
    {
        nancyModule.After.AddItemToEndOfPipeline(CheckForCached);
    }

    static void CheckForCached(NancyContext context)
    {
        var responseHeaders = context.Response.Headers;
        var requestHeaders = context.Request.Headers;

        string currentFileEtag;
        if (responseHeaders.TryGetValue("ETag", out currentFileEtag))
        {
            if (requestHeaders.IfNoneMatch.Contains(currentFileEtag))
            {
                context.Response = HttpStatusCode.NotModified;
                return;
            }
        }

        string responseLastModifiedString;
        if (responseHeaders.TryGetValue("Last-Modified", out responseLastModifiedString))
        {
            var responseLastModified = DateTime.ParseExact(responseLastModifiedString, "R", CultureInfo.InvariantCulture, DateTimeStyles.None);
            if (responseLastModified <= requestHeaders.IfModifiedSince)
            {
                context.Response = HttpStatusCode.NotModified;
            }
        }
    }
}

An interesting point to note here is that I am using AddItemToEndOfPipeline. So this means this happens * after * your code returns the response to Nancy. This may seem counter intuitive, because the cache check should happen before the response, however this is by design. The reason for this is that Nancy delays streaming through the use of a delegate. So this cache check will happen after you returned your response but before the delegate is executed and the true response is returned to the caller.

Performance Improvements

So all this is done to get some more performance...

* Large File (20MB) *

  • Normal Response: 456ms
  • Cached Response: ~1ms

* Medium File (250KB) *

  • Normal Response: 9ms
  • Cached Response: ~1ms

* Small File (10KB) *

  • Normal Response: 4ms
  • Cached Response: ~1ms

As you can see this approach can provide significant performance improvements. Especially for larger responses.

* Note: * I had to hack the response code to get these results. Due to the fact that my tests are written in .net I suffer from the fact that .net throws an exception for a "Not Modified" response. It is the same problem as described here How to avoid WebException for 403 Forbidden response?. So to work around this I temporarily return HttpStatusCode.Ok so I can test the performance without an exception.

Sample Code

There is a full working sample here:

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

Posted by: Simon Cropp
Last revised: 20 Dec, 2011 07:36 PM History

Comments

No comments yet. Be the first!

No new comments are allowed on this post.