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);
}
}
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;
}
}
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);}
}
Nice post Javi!
ReplyDeleteWhy not change your implementation to limit the antipatterns i.e. don't accept an unsafe method like POST over a safe method like GET?
Hi Glenn,
ReplyDeleteGlad that you liked it!
Indeed maybe if I make the implementation more complete we could add the channel to the Contrib project.
Yes that would be perfect! Talk to @darrel_miller on twitter if you are interested in contributing.
ReplyDelete