This post is over 6 months old. Some details, especially technical, may have changed.

Nancy JSONP Hook

3 Nov 2011: As of Nancy v0.9 this is part of the core (same logic applies)

I needed to write a simple JSONP capable service recently and decided to run with Nancy. Turns out Nancy doesn't give us a way to do JSONP so I set about creating something that fitted my use case. The fruits of my very very short labour (thanks to the super-hyper-flexible-powerfulness of the Nancy Pipeline) can be seen here. The full source is available in the Github repo and hopefully we can get this pushed into the core of Nancy shortly (or at least part of the official repo one way or another) - just for the geek cred of course :)

Implementation Rundown

I took the same approach as the existing hooks (using the SassAndCoffee one as my implementation pattern) so created a static Hooks class with an Enable method that simply adds a new item to the end of the pipeline

/// <summary>
/// Enable JSONP support in the application
/// </summary>
/// <param name="pipeline">Application Pipeline to Hook into</param>
public static void Enable(IApplicationPipelines pipeline)
{
    pipeline.AfterRequest.AddItemToEndOfPipeline(PrepareJsonp);
}

The meat of the work lies in the PrepareJsonp method

/// <summary>
/// Transmogrify original response and apply JSONP Padding
/// </summary>
/// <param name="context">Current Nancy Context</param>
private static void PrepareJsonp(NancyContext context)
{
    bool isJson = context.Response.ContentType == "application/json";
    bool hasCallback = context.Request.Query["callback"].HasValue;

    if (isJson && hasCallback)
    {
        // grab original contents for running later
        Action<Stream> original = context.Response.Contents;
        string callback = context.Request.Query["callback"].Value;

        // set content type to application/javascript so browsers can handle it by default
        // http://stackoverflow.com/questions/111302/best-content-type-to-serve-jsonp
        context.Response.ContentType = "application/javascript";
        context.Response.Contents = stream =>
        {
            // disposing of stream is handled elsewhere
            StreamWriter writer = new StreamWriter(stream)
            {
                AutoFlush = true
            };

            writer.Write("{0}(", callback);
            original(stream);
            writer.Write(");");
        };
    }
}

Let me highlight a few of the key areas here.

Determine if Response need JSONP Padded

I decided to take a basic approach to determining if the request should return a JSONP response,

  • If the content type of the current response is application/json
  • If there is a query string parameter called callback

Here is what we need to satisfy that,

bool isJson = context.Response.ContentType == "application/json";
bool hasCallback = context.Request.Query["callback"].HasValue;

if (isJson && hasCallback)
{

I am sure this could be refined and open to suggestion (perhaps more applicable content types or extra possible configuration for the callback parameter). I wonder if it is a bit safer to restrict requests that are GET requests seeing as that is the only way to do JSONP? Hmmmm.

Next thing I set the content type to application/javascript as that makes perfect sense

context.Response.ContentType = "application/javascript";

Finally I wrap the original responses content in the JSONP "padding" (the callback function) and write everything out.

context.Response.Contents = stream =>
{
    // disposing of stream is handled elsewhere
    StreamWriter writer = new StreamWriter(stream)
    {
        AutoFlush = true
    };

    writer.Write("{0}(", callback);
    original(stream);
    writer.Write(");");
};

So all in all very simple but damn I'm proud that I got off my arse and contributed to such a stellar community. hopefully someone finds this useful.

Published in .NET on November 01, 2011