Согласно стандарту HTTP/1.1 метод OPTIONS может быть использован клиентом для определения параметров или требований, связанных с ресурсом. Сервер также может отправлять документацию в удобочитаемом формате. Ответ на запрос OPTIONS может содержать список допустимых методов для данного ресурса в хедере Allow.
То есть этот метод мог бы стать отличным средством для документирования наших REST-сервисов с одной стороны, и быть существенным дополнением к архитектурному ограничению HATEOAS с другой.
А теперь давайте отвлечёмся от страшных слов типа “HATEOAS” и зададимся вопросом: а есть ли какая-нибудь практическая польза от использования метода OPTIONS в веб-приложениях?
Итак, по минимуму, ответ на запрос OPTIONS должен содержать список методов, допустимых для данной конечной точки или попросту Uri, в хедере Allow.
HTTP/1.1 200 OK
Allow: GET,POST,DELETE,OPTIONS
Content-Length: 0
Что это даёт?
Представьте себе, что у нас есть веб-приложение, позволяющее размещать ресурсы и работать с ними. Ну, например, что-то вроде Google Docs. Каждый пользователь имеет определённые права на документ: кто-то может читать его, кто-то редактировать, а кто-то удалять.
Перед нами стоит задача разработки пользовательского интерфейса. Грубо говоря, нам надо в определённый момент решить, каким образом мы будем прятать или показывать кнопку Delete в зависимости от полномочий текущего пользователя.
Можно получать знания о полномочиях из сервиса и реализовывать логику на стороне клиента. Но это плохой подход.
В REST-архитектуре клиент и сервер должны быть максимально независимыми. Не дело клиента выяснять, какими полномочиями должен обладать пользователь, чтобы иметь возможность удалить документ. Если клиент будет реализовывать свою логику, то изменение механизма полномочий на сервере приведёт, скорее всего, к необходимости изменений на клиенте.
Если же мы будем иметь возможность запросить OPTIONS по Uri документа и получить список допустимых методов, задача решается просто: если хедер Allow содержит “Delete”, значит, кнопку надо показать, иначе – спрятать.
Реализовать метод OPTIONS нетрудно.
[HttpOptions]
[ResponseType(typeof(void))]
[Route("Books", Name = "Options")]
public IHttpActionResult Options()
{
HttpContext.Current.Response.AppendHeader("Allow", "GET,OPTIONS");
return Ok();
}
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);
}
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;
}
}
}
configuration.MessageHandlers.Add(new OptionsHandler());
[Route("Books/{id:long}", Name = "GetBook")]
К сожалению, не доступен сервер mySQL