Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

How to Implement OPTIONS Response in ASP.NET Web API 2

0.00/5 (No votes)
13 Nov 2017 1  
Automation of OPTIONS response in REST-service

HTTP OPTIONS verb is a good opportunity to make REST-service self-documented: http://zacstewart.com/2012/04/14/http-options-method.html.

Minimal requirement is to have Allow header in response content, enumerating all available methods for the given URI, so implementation is rather easy:

[HttpOptions]
[ResponseType(typeof(void))]
[Route("Books", Name = "Options")]
public IHttpActionResult Options()
{
    HttpContext.Current.Response.AppendHeader("Allow", "GET,OPTIONS");
    return Ok();
}

Response:

HTTP/1.1 200 OK
Allow: GET,OPTIONS
Content-Length: 0

However, this approach has several serious shortcomings. We have to maintain the list of supported methods manually and we have to repeat this for each controller and for every supported URI. It’s very easy to forget to add appropriate OPTIONS action when we introduce new URI. So, it would be great to add some automation here.

ASP.NET Web API provides a nice way to have a more high-level solution: HTTP message handlers.

I inherited a new handler from DelegatingHandler, overwrote SendAsync method and added my functionality as continuation of base task. This is important because I want to run basic route mechanism before any processing. In that case, request will contain all needed properties.

protected override async Task<HttpResponseMessage> SendAsync(
    HttpRequestMessage request, CancellationToken cancellationToken)
{
    return await base.SendAsync(request, cancellationToken).ContinueWith(
        task =>
        {
            var response = task.Result;
            if (request.Method == HttpMethod.Options)
            {
                var methods = new ActionSelector(request).GetSupportedMethods();
                if (methods != null)
                {
                    response = new HttpResponseMessage(HttpStatusCode.OK)
                    {
                        Content = new StringContent(string.Empty)
                    };

                    response.Content.Headers.Add("Allow", methods);
                    response.Content.Headers.Add("Allow", "OPTIONS");
                }
            }

            return response;
        }, cancellationToken);
}

Class ActionSelector attempts to find the appropriate controller for the given request in constructor. If controller not found, method GetSupportedMethods returns null. Function IsMethodSupported makes a trick, creating new request with required method, and checks if action is found. Finally block restores old routeData in context because _apiSelector.SelectAction call can change it.

private class ActionSelector
{
    private readonly HttpRequestMessage _request;
    private readonly HttpControllerContext _context;
    private readonly ApiControllerActionSelector _apiSelector;
    private static readonly string[] Methods = 
    { "GET", "PUT", "POST", "PATCH", "DELETE", "HEAD", "TRACE" };

    public ActionSelector(HttpRequestMessage request)
    {
        try
        {
            var configuration = request.GetConfiguration();
            var requestContext = request.GetRequestContext();
            var controllerDescriptor = new DefaultHttpControllerSelector(configuration)
                .SelectController(request);

            _context = new HttpControllerContext
            {
                Request = request,
                RequestContext = requestContext,
                Configuration = configuration,
                ControllerDescriptor = controllerDescriptor
            };
        }
        catch
        {
            return;
        }

        _request = _context.Request;
        _apiSelector = new ApiControllerActionSelector();
    }

    public IEnumerable<string> GetSupportedMethods()
    {
        return _request == null ? null : Methods.Where(IsMethodSupported);
    }

    private bool IsMethodSupported(string method)
    {
        _context.Request = new HttpRequestMessage(new HttpMethod(method), _request.RequestUri);
        var routeData = _context.RouteData;

        try
        {
            return _apiSelector.SelectAction(_context) != null;
        }
        catch
        {
            return false;
        }
        finally
        {
            _context.RouteData = routeData;
        }
    }
}

The last step is adding our message handler to configuration in start-up code:

configuration.MessageHandlers.Add(new OptionsHandler());

With this approach, REST service will create OPTIONS response for every valid URI.

Don’t forget to specify parameter types. If you use attribute routing, specify type in Route attribute:

[Route("Books/{id:int}", Name = "GetBook")]

Without implicit specification, ActionSelector will consider path “Books/abcd” as valid.

But this is still not a good solution. First of all, the handler does not take into account authorization. Secondly, the service will respond on request, even if URI contains wrong resource ID, for example /books/0. This dramatically reduces the practical advantage of using OPTIONS verbs.

If we want authorization of resources, we have to add controller action OPTIONS for each URI that contains resource Ids, because only controllers can authorize resources. At that, our handler should use response from the action if any.

Final implementation:

protected override async Task<HttpResponseMessage> SendAsync(
    HttpRequestMessage request, CancellationToken cancellationToken)
{
    return await base.SendAsync(request, cancellationToken).ContinueWith(
        task =>
        {
            var response = task.Result;
            switch (GetResponseAction(request, response))
            {
                case ResponseAction.UseOriginal:
                    return response;
                case ResponseAction.ReturnUnauthorized:
                    return new HttpResponseMessage(HttpStatusCode.Unauthorized);
                case ResponseAction.ReturnUnauthorized:
                    var methods = new ActionSelector(request).GetSupportedMethods();
                    if (methods == null)
                        return response;

                    response = new HttpResponseMessage(HttpStatusCode.OK)
                    {
                        Content = new StringContent(string.Empty)
                    };

                    response.Content.Headers.Add("Allow", methods);
                    response.Content.Headers.Add("Allow", "OPTIONS");
                    return response;
                default:
                    throw new InvalidOperationException("Unsupported response action code");
            }
        }, cancellationToken);
}

private enum ResponseAction
{
    UseOriginal,
    RetrieveMethods,
    ReturnUnauthorized
}

private static ResponseAction GetResponseAction(
    HttpRequestMessage request, HttpResponseMessage response)
{
    if (request.Method != HttpMethod.Options)
        return ResponseAction.UseOriginal;

    if (response.StatusCode != HttpStatusCode.MethodNotAllowed)
        return ResponseAction.UseOriginal;

    // IsAuthenticated() returns true if current user is authenticated
    return IsAuthenticated() ? ResponseAction.RetrieveMethods : ResponseAction.ReturnUnauthorized;
}

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here