Обработка исключений в WebAPI

Обработка исключений - одна из важнейших тем, о которой часто забывают и оставляют "на потом". Типичные поведение веб-приложения при возникновении необработанного исключения - ответить клиенту с кодом HTTP 500 и формулировкой "что-то пошло не так".

Это логично, когда мы генерируем HTML-страницы. Но реализация REST API часто требует от нас другого. Например, при появлении необработанного исключения из предметной области выдавать какой-то специфичный код возврата, чтобы наши клиенты знали в чем проблема.

Первое, что приходит в голову — добавить в контроллер логику, которая будет генерировать нужные коды возврата для каждой ситуации.

В WebAPI есть такой механизм — Exception Filter — он позволяет для таких ситуаций создать более прозрачный код.

Создание Exception Filter

Например, имеется WebAPI контроллер, который на определенном наборе данных генерирует NotSupportedException:

public class UsersController : ApiController
{
  [HttpPost]
  public void Create(UsersCreateModel model)
  {
    if (....)
      throw new NotSupportedException();
  }
}

Если обратиться к такому методу через HTTP и смоделировать ситуацию, когда выбрасывается данное исключение, то в ответ будет отправлено сообщение похожее на такое:

HTTP/1.1 500 Internal Server Error
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
Content-Length: 2687

{"Message":"An error has occurred.","ExceptionMessage": ...

Если приложение запущено в отладочной среде, здесь даже будет содержаться информация о типе исключения с подробным описанием.

Клиент не всегда ожидает подобного поведения. К примеру, если он отправил некорректный набор данных почему бы не вернуть ему Bad Request с описанием причины? Чтобы переопределить это поведение создадим ExceptionFilter - наследник ExceptionFilterAttribute, в котором переопределен метод OnException:

public class NotSupportedExceptionFilterAttribute : ExceptionFilterAttribute
{
  public override void OnException(HttpActionExecutedContext context)
  {
    if (context.Exception is NotSupportedException)
    {
      context.Response = new HttpResponseMessage(HttpStatusCode.BadRequest)
      {
        Content = new StringContent("Data you provided is not supported.")
      };
    }
  }
}

Как видно из этого кода, этот фильтр проверяет тип обрабатываемого исключения и при необходимости подменяет ответ. Осталось только подключить этот фильтр к приложению.

Подключение к приложению

Подключить Exception Filter можно двумя способами:

  1. Разметить атрибутом нужный контроллер/действия
  2. Подключить глобально

Если логика работы данного фильтра актуальна только для конкретного контроллера или действия, то имеет смысл разметить их данным атрибутом, чтобы фильтр начал работу:

public class UsersController : ApiController
{
  [HttpPost]
  [NotSupportedExceptionFilter]
  public void Create(UsersCreateModel model)
  {
    throw new NotSupportedException();
  }
}

Возможна такая ситуация, когда логика фильтра должна работать для всего приложения. Например, мы обрабатываем все исключения, связанные с бизнес логикой и формируем корректные ответы. В этом случае можно не размечать контроллеры данным атрибутом, а добавить их при конфигурировании глобально:

public static class WebApiConfig
{
  public static void Register(HttpConfiguration config)
  {
    config.Filters.Add(new NotSupportedExceptionFilterAttribute());

Теперь при выбрасывании необработанных исключений NotSupportedException, клиенту будет отправлен HTTP-ответ следующего вида:

HTTP/1.1 400 Bad Request
Cache-Control: no-cache
Pragma: no-cache
Content-Length: 32
Content-Type: text/plain; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0

Data you provided is not supported.