Saturday, April 16, 2011

Tunnelling PUT and DELETE with WCF Web APIs Preview 4

PUT and DELETE are first class citizen HTTP methods in REST resource implementations but unfortunately not all clients support these methods and in fact the XHTML4 specification defines GET and POST as the only valid Form methods. Browsers can use Javascript and the XmlHttpRequest to get around this limitation, but other issues such as the fact that some firewalls block PUTs and DELETE requests, recommend having an alternative strategy to convey these methods semantics in the cleanest possible way.

Existing REST frameworks implement a tunnelling technique that solves this issue: clients make POST request and provide the “real” method somewhere in the request. For example, Ruby On Rails uses a hidden form field called _method and users of the Google GData API store the real method in the X-HTTP-Method-Override Custom HTTP header.

Implementing PUT and DELETE tunnelling is a very easy task thanks to the new extensibility point: Message Channels. Message Channels can influence what Resource Method is to be executed by the framework and that’s precisely what we’re going to do:

public class HttpMethodTunnelChannel : DelegatingChannel
{
    public HttpMethodTunnelChannel(HttpMessageChannel innerChannel)
        : base(innerChannel)
    {
    }
 
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, 
                                                           CancellationToken cancellationToken)
    {
        request.SetOverrideMethodIfAny();
        return base.SendAsync(request, cancellationToken);
    }
}
SetOverrideMethodIfAny is a custom extension method of HttpResponseMessage.

public static class HttpRequestMessageExtensions
{
    public static void SetOverrideMethodIfAny(this HttpRequestMessage request)
    {
        var method = request.GetOverrideMethod();
        if (method != null) request.Method = method;
    }
 
    public static HttpMethod GetOverrideMethod(this HttpRequestMessage request)
    {
        var method = HttpUtility.ParseQueryString(request.RequestUri.Query)["_method"];
 
        if (String.IsNullOrEmpty(method))
            method = request.Headers
                .Where(h => h.Key == "X-HTTP-Method-Override")
                .SelectMany(h => h.Value)
                .FirstOrDefault();
 
        return MapMethod(method);
    }
 
    private static HttpMethod MapMethod(string method)
    {
        if (String.IsNullOrEmpty(method)) return null;
        if (String.Compare(method, HttpMethod.Put.Method, true) == 0)
            return HttpMethod.Put;
        if (String.Compare(method, HttpMethod.Delete.Method, true) == 0)
            return HttpMethod.Delete;
        if (String.Compare(method, HttpMethod.Options.Method, true) == 0)
            return HttpMethod.Options;
        if (String.Compare(method, HttpMethod.Head.Method, true) == 0)
            return HttpMethod.Head;
        return null;
    }
}
In the sample, the query string _method takes precedence over the custom HTTP Header. Bear in mind that the code above not only tunnels PUT and DELETE requests, clients could actually do weird things such as tunnelling GETs over POST or the non-recommended practice of tunnelling unsafe operations (i.e. DELETE) over safe requests (i.e.: GET) which could end up with Google deleting your data when it crawls your site!

UPDATE: You can find a more complete implementation here. The idea is that safe methods should not become unsafe when tunnelled and the other way around.

Finally, let’s use the new fluent API to bind our message channel.

public class Global : System.Web.HttpApplication
{
    protected void Application_Start(object sender, EventArgs e)
    {
        var config = HttpHostConfiguration.Create().
            AddMessageHandlers(typeof(HttpMethodTunnelChannel));
 
        RouteTable.Routes.MapServiceRoute<ContactResource>("Contact", config);
    }
}

And that’s it! As you can see, I’ve just scratched the surface of Message Channels and you may have already realized their potential for implementing cross cutting concerns such as logging, exception handling or security for example.

3 comments:

  1. Nice post Javi!

    Why not change your implementation to limit the antipatterns i.e. don't accept an unsafe method like POST over a safe method like GET?

    ReplyDelete
  2. Hi Glenn,

    Glad that you liked it!
    Indeed maybe if I make the implementation more complete we could add the channel to the Contrib project.

    ReplyDelete
  3. Yes that would be perfect! Talk to @darrel_miller on twitter if you are interested in contributing.

    ReplyDelete