Jef Claes

On software and life

02 Sep 2012

Supporting the OPTIONS verb in ASP.NET Web API

ASP.NET Web API controllers support only four HTTP verbs by convention: GET, PUT, POST and DELETE. The full list of existing HTTP verbs is more extensive though. One of those unsupported verbs which can be particularly useful for API discovery and documentation is the OPTIONS verb.

The OPTIONS method represents a request for information about the communication options available on the request/response chain identified by the Request-URI. This method allows the client to determine the options and/or requirements associated with a resource, or the capabilities of a server, without implying a resource action or initiating a resource retrieval.

If we wanted to implement controller support for the OPTIONS verb, we could manually map an action to the verb by decorating it with an AcceptVerbs attribute, and making the action return a response with a relevant Access-Control-Allow-Methods header.

For a values controller, with a GET and DELETE action, it could look like this.

public class ValuesController : ApiController
{        
    public IEnumerable<string> Get()
    {
        return new string[] { "value1", "value2" };
    }                 
    
    public void Delete(int id) { }

    [AcceptVerbs("OPTIONS")]
    public HttpResponseMessage Options()
    {
        var resp = new HttpResponseMessage(HttpStatusCode.OK);
        resp.Headers.Add("Access-Control-Allow-Origin", "*");
        resp.Headers.Add("Access-Control-Allow-Methods", "GET,DELETE");

        return resp;
    }
}

If we now make an OPTIONS request to http://localhost:53314/api/values..

OPTIONS http://localhost:53314/api/values HTTP/1.1
User-Agent: Fiddler
Host: localhost:53314

..we receive following response.

HTTP/1.1 200 OK
Server: ASP.NET Development Server/10.0.0.0
Date: Sun, 02 Sep 2012 13:46:21 GMT
X-AspNet-Version: 4.0.30319
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET,DELETE
Cache-Control: no-cache
Pragma: no-cache
Expires: -1
Content-Length: 0
Connection: Close

While this works, it’s not a great solution. We don’t want to maintain the list of supported verbs manually, and we certainly don’t want to repeat this for each controller.

ASP.NET Web API provides a nice way to have a more high level solution: HTTP message handlers. These basically behave as HTTP intermediaries, meaning we can intercept each OPTIONS request early, and bypass the whole request chain. Another useful component we can make good use of is the default ApiExplorer; this abstraction allows us to obtain metadata of our API’s stucture.

To create a new HTTP message handler, I inherited from the DelegatingHandler class, and overwrote the SendAsync method. Here we’ll intercept the request when the verb equals OPTIONS. An instance of the Api explorer can be resolved through the global configuration. I grab the requested controller from the request route data, and use that to search the Api explorer’s ApiDescriptions collection for its associated actions, and its associated verbs. Once I get the outcome of this lookup, and at least one verb is supported (*), I’ll return an OK response with the relevant headers, and thus short circuit the request.

public class OptionsHttpMessageHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (request.Method == HttpMethod.Options)
        {
            var apiExplorer = GlobalConfiguration.Configuration.Services.GetApiExplorer();

            var controllerRequested = request.GetRouteData().Values["controller"] as string;              
            var supportedMethods = apiExplorer.ApiDescriptions
                .Where(d => 
                {  
                    var controller = d.ActionDescriptor.ControllerDescriptor.ControllerName;
                    return string.Equals(
                        controller, controllerRequested, StringComparison.OrdinalIgnoreCase);
                })
                .Select(d => d.HttpMethod.Method)
                .Distinct();

            if (!supportedMethods.Any())
                return Task.Factory.StartNew(
                    () => request.CreateResponse(HttpStatusCode.NotFound));

            return Task.Factory.StartNew(() =>
            {
                var resp = new HttpResponseMessage(HttpStatusCode.OK);
                resp.Headers.Add("Access-Control-Allow-Origin", "*");
                resp.Headers.Add(
                    "Access-Control-Allow-Methods", string.Join(",", supportedMethods));

                return resp;
            });
    }

    return base.SendAsync(request, cancellationToken);
    }
}

Register this HTTP message handler by adding it to the configuration.

GlobalConfiguration.Configuration.MessageHandlers.Add(new OptionsHttpMessageHandler());

This second solution still yields the same result, but is far more scalable.

(*) When a resource can’t be read nor manipulated, it must not exist right?