📜 ⬆️ ⬇️

Do I need the OPTIONS method in REST services?

According to the HTTP / 1.1 standard, the OPTIONS method can be used by the client to determine the parameters or requirements associated with the resource. The server can also send documentation in a readable format. The response to an OPTIONS request may contain a list of valid methods for this resource in the Allow header.

That is, this method could be an excellent tool for documenting our REST services on the one hand, and be a significant addition to the architectural constraint of HATEOAS on the other.

And now let's take a look at the scary words like “HATEOAS” and ask ourselves: is there any practical benefit from using the OPTIONS method in web applications?

So, at a minimum, the response to an OPTIONS request must contain a list of methods that are valid for this endpoint or simply Uri, in the Allow header.
')
HTTP/1.1 200 OK
Allow: GET,POST,DELETE,OPTIONS
Content-Length: 0

What does this give?

Imagine that we have a web application that allows you to post resources and work with them. Well, for example, something like Google Docs. Each user has certain rights to the document: someone can read it, edit it, and delete it.

We are faced with the task of developing a user interface. Roughly speaking, we need to decide at some point how we will hide or show the Delete button depending on the current user's credentials.

You can get knowledge about the authority from the service and implement the logic on the client side. But this is a bad approach.

In the REST architecture, the client and server should be as independent as possible. It is not up to the client to find out what authority the user must have in order to be able to delete the document. If the client implements his logic, then a change in the mechanism of authority on the server will most likely result in the need for changes on the client.

If we will be able to request OPTIONS for the Uri of the document and get a list of acceptable methods, the problem is solved simply: if the Allow header contains “Delete”, it means that the button must be shown, otherwise hide.

Implementing the OPTIONS method is not difficult.

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

But implementing the methods for each controller and each route is difficult. The problem is that maintaining this economy is rather troublesome: it is very easy to forget to add the appropriate method to the controller, or to make a mistake when changing the routing.

Is it possible to automate the process?

ASP.NET Web API provides a good opportunity for automation: HTTP Message Handlers .

Inherit our new handler from the DelegatingHandler class, overload the SendAsync method , and add new functionality as a continuation of the task. This is important because we want to start the basic routing mechanism first. In this case, the request variable will contain all the necessary 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); } 

The ActionSelector class tries to find a suitable controller for the query in the constructor. If the controller is not found, the GetSupportedMethods method returns null. The IsMethodSupported function replaces the current request in context in order to find the action method by the specified name. The finally block restores RouteData , since a call to _apiSelector.SelectAction can change this context property.

 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 final step is to add our handler to the configuration in the startup code:

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

For everything to work correctly, you must explicitly specify the types of parameters. If the Route attribute is used, the type must be specified directly in it:

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

Without an explicit type indication, the path Books / abcd will be considered as correct.

So, now we have OPTIONS implementation for all supported Uri in the service. However, this is still not the perfect solution. The described approach does not take into account authorization. If we use access tokens and each service call provides for the current principal, then its rights are not taken into account and the value of the OPTIONS method sharply decreases. For any user, the client will always receive the same list of valid HTTP methods, regardless of their rights.

The way out of this is to manually add the HttpOptions implementations to the controllers where necessary. These action methods should verify the user's rights when creating a list of valid methods. In this case, our OptionsHandler should use the response from the controller, if available.

However, there is a problem with CORS. In case of cross-domain requests, the browser will independently send an OPTIONS request to the requested address, and then we have no opportunity to install the Authorization header.

Thus, we must implement OPTIONS in the service for both authorized and unauthorized users. In the first case, we will take into account the rights of the current user, and in the second we will have to return all valid methods.

→ Final Code



Author's translation

Source: https://habr.com/ru/post/342432/


All Articles