Sunday, September 2, 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? 

13 comments:

  1. Would you mind adding this to WebApiContrib? This would really be useful.

    ReplyDelete
  2. This comment has been removed by the author.

    ReplyDelete
  3. OPTIONS verbs should be much more than HTTP headers?

    It's should describe hypermedia and links like this:

    OPTIONS /questions HTTP/1.1
    Host: domain.com
    Accept: text/html, text/xml, application/json
    A possible response could be:

    HTTP/1.1 200 Ok
    Allow: GET, POST
    Content-Type: text/xml







    How WebAPI can help us in best way?

    ReplyDelete
  4. This comment has been removed by the author.

    ReplyDelete
  5. Comment box drop XML-SCHEMA data.

    Please see what I'm saying at http://stackoverflow.com/questions/5315524/auto-documenting-rest-api-in-php/10843583#10843583

    ReplyDelete
    Replies
    1. I see, but it depends on interpretation I'm afraid.

      The response body, if any, SHOULD also include information about the communication options. The format for such a body is not defined by this specification, but might be defined by future extensions to HTTP.

      You could do some more research on the ApiExplorer, and try to extract the information you want, to eventually write it to the response body yourself.

      Delete
  6. Thanks Jef/Alexandro, I've been researching the ApiExplorer and been trying to write the response to the body. Haven't had much luck yet. Please let me know if either of you try.

    ReplyDelete
  7. ASP.NET Web API provides a nice way to have a more high level solution like HTTP message handlers.

    ReplyDelete
  8. Thanks Jef,

    Great article. I have one question that you might be able to help me with. I have implemented both these approaches successfully on localhost but when I run it on Win Server 2012 the headers don't get added. Any idea why this might be happening? I can access the ApiController (in the first example) as the service returns data but the Options function never seems to get called. Is there something that has to be configured on the server to get this working?

    Thanks,
    Frank

    ReplyDelete
    Replies
    1. It's possible that IIS doesn't let the verb through. Have you read this SO question? http://stackoverflow.com/questions/6147181/405-method-not-allowed-in-iis7-5-for-put-method

      Delete
    2. Thanks. That's great. Removing the WebDAV module from my app seems to fix the problem. I wonder why this would be the default behaviour for an MVC app?

      Delete
  9. Lovely. Thanks for this !

    ReplyDelete