HEAD HTTP verb is defined in RFC 2616 as “identical to GET except that the server MUST NOT return a message-body in the response.” As such you can think of it as a twin brother of GET.
There are a lot of use cases for HEAD: pinging without the overhead of transferring data, or simply requesting information about the size of a resource (which can be used to provide download progress bar), just to name a few.
Unfortunately, out of the box, ASP.NET Web API doesn’t provide a mechanism of supporting HEAD or coupling GET & HEAD.
We can work around that.
How is it now? π
You’d think it might be as simple as adding [AcceptVerbs(“HEAD”)] to your action?
[AcceptVerbs("GET", "HEAD")]
public HttpResponseMessage Get(int id)
{
}
Unfortunately, in the default Verb-based dispatching model, Web API will not find such action for a HEAD request and respond with 404.
If you use RPC dispatching:
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
Your odds are little better, because the action will be found and hit correctly, but:
- you probably don’t want to ditch RESTful routes just for HEAD support
- the behavior of the HEAD will not be complete anyway, because it will respond with Content-Length: 0 rather then the content length of the resource it represents.
HTTP HEAD trickery with MessageHandler π
MessageHandlers, due to their nature of working closely with HTTP request/response and running first & last in the pipeline, are a perfect tool to solve this problem for us.
As a HEAD request flows into the system, we can switch it from HEAD to GET, and it will be processed as such. Then, at the end of the pipeline, we will just strip out the body (content) of the response.
The implementation would look like this:
public class HeadHandler : DelegatingHandler
{
private const string Head = "IsHead";
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
{
if (request.Method == HttpMethod.Head)
{
request.Method = HttpMethod.Get;
request.Properties.Add(Head, true);
}
var response = await base.SendAsync(request, cancellationToken);
object isHead;
response.RequestMessage.Properties.TryGetValue(Head, out isHead);
if (isHead != null && ((bool) isHead))
{
var oldContent = await response.Content.ReadAsByteArrayAsync();
var content = new StringContent(string.Empty);
content.Headers.Clear();
foreach (var header in response.Content.Headers)
{
content.Headers.Add(header.Key, header.Value);
}
content.Headers.ContentLength = oldContent.Length;
response.Content = content;
}
return response;
}
}
So if we have an incoming HEAD request, we switch it to GET and save a flag inside HttpRequestMessage. Once the entire processing pipeline runs, we try to retrieve the flag from the request and if it’ there, we reset the content of the response to string.Empty.
We then copy the content headers from old HttpContent to the new one as they would be nullfied as soon as new HttpContent is set. This can be done very easily in a loop because HttpContentHeaders is actually an IEnumerable<KeyValuePair<string, IEnumerable
The only exception to the copying is the Content-Length header which the framework will compute for us based on the HttpContent. Since we just set it to string.Empty that would obviously be 0 which is not desirable. We can work around that by reading the byte length of the original body and set that manually.
As a result we end up with a complete response to a GET, including Content-Length and Content-Type - but without the content body.
This can now be registered against the configuration - and voila - you have HEAD support for all your GET methods.
config.MessageHandlers.Add(new HeadHandler());
And that’s it.