Денис Крешихин

Денис
Крешихин

iOS-разработчик с 15+ летним опытом

Тимлид/сеньор по обстоятельствам

Интересы: swift, uikit, rxswift, oop/ood, devops, agile

2024 © Денис Крешихин

Обработка ошибок в RESTful — приложениях

За последнее время очень многие веб-фреймворки обзавелись RESTful роутингом. Более того, REST стал де-факто стандартом проектирования архитектуры веб-приложений. Практически все более-менее значимые сервисы обзавелись RESTful API с представлением данных через xml и json форматы. Такой популярности REST помогло как появление большого количества руководств, так и горячие обсуждения REST среди специалистов.

Вместе с тем, REST до сих пор воспринимается скорее как некоторый набор правил роутинга, а всё что не связано в прямую с роутингом решается произвольным путём, в частности это касается обработки ошибок в RESTful-приложениях.

Обработка ошибок на программном уровне

Рассмотрим некоторое веб-приложение в котором пользователь может динамически добавить статью article в список статей self.articles. Обычно при динамической реализации используется такой подход:

  1. Собираются данные с формы и формируется некоторый объект с данными
  2. Данные отправляются через post/put запрос на сервер
  3. Дальнейшее выполнение программы зависит от состояния флага успешности в ответе, например response.status = 'ok'

Т.е. в коде на javscript + jQuery и Ruby это могло бы выглядеть так:

$.post('articles', {title: 'title', text: 'text'}, 'json').done(function(article_data){
    if(response.status == 'ok'){
        self.articles.push(article_data);
    }else{
        var messages = response.messages;
        // Обработать ошибку валидации и вывести сообщения
    }
}).fail(function(response){
    // Обработать ошибку сервера или соединения
});

А это пример контроллера на Ruby on Rails. Это далеко не идеальный код, но часто встречающийся в таком виде. Результат выполнения метода create не влияет на статус код HTTP:

class ArticlesController < ApplicationController
    def create
        @article = Article.new
        @article.title = params[:title]
        @article.text = params[:text]

        # ... какие-то действия ...

        if not @article.valid?
            render json: {status: 'error', messages: @article.errors.messages}
        end

        @article.save

        render json: @article
    end
end

Плюсы:

  1. Можно делать сколь угодно сложный интерфейс для обработки ошибок.
  2. Нет ограничений на количество состояний.
  3. Не требуется знаний всех тонкостей HTTP (коих очень много).

В таком подходе есть несколько слабых мест:

  1. Нарушается семантика ответов на прикладном уровне, невалидный запрос может получить ответ 200 OK
  2. Прикладной уровень и программный дублируют друг друга. Так в случае успеха, появляются две проверки на уровне XMLHttpRequest и на уровне пользовательского кода, в виде проверки response.status == 'ok'
  3. Требуется спецификация интерфейса, что бы знать какие поля отвечают за состояние

Обработка ошибок на прикладном уровне

Т.к. RESTful уже подразумевает наложение некоторых ограничений, то такой же подход можно применить и по отношению к ошибкам. Т.е. использовать для обработки ошибок статус коды HTTP, подобно тому как ресурсы отображают доступные модели и контроллеры. Так, например, для уведомления о невалидных данных можно использовать ошибку 422 Unprocessable Entity, а список невалидных полей передавать непосредственно в виде массива в теле ответа:

$.post('articles', {title: 'title', text: 'text'}, 'json').done(function(response){
    var article = response.data;
    self.articles.push(article);
}).fail(function(e, ){
    switch(e.status){
    case 422:
        var messages = response.responseText;
        // Обработать ошибку валидации и вывести сообщения
    default:
        // Обработать ошибку сервера или соединения
    }
});

При реализации контроллера есть смысл разнести обработку ошибок по разным rescue блокам. Так, в случае если поля окажутся невалидными, выполнится первый rescue-блок. Если случится какая-то иная ошибка с валидными данными, то сработает последний rescue. Таким образом, на стороне клиента можно отлавливать ошибки без привлечения дополнительных статус-полей в json.

class ArticlesController < ApplicationController
    def create
        @article = Article.new
        @article.title = params[:title]
        @article.text = params[:text]

        # ... какие-то действия ...

        # ! возбуждает исключение если есть невалидные поля
        @article.save!

        render json: @article
    rescue Mongoid::Errors::Validations
        render json: @article.errors.messages, status: 422
    rescue
        render text: 'Internal server error', status: 500
    end
end

Плюсы:

  1. Семантика кодов ошибок HTTP и приложения совпадают, подобно тому как совпадают модели с названием ресурсов, а методы контроллера с методами HTTP.
  2. Исключается дублирования программного и прикладного уровней.
  3. Не требуется разработка спецификаций ошибок.

Недостатки:

  1. Не всегда существует нужный код ошибки
  2. Необходимо хорошо знать особенности HTTP (статус код может повлиять на работу браузера)

Как вариант, для расширения существующих статусов можно добавлять поля в заголовок, например X-Status-Reason: Validation failed

В 70% случаев мне удаётся ограничится данными статус кодами HTTP:

200 OK Штатный ответ, у пользователя есть права на доступ к ресурсу
404 Not Found Ресурса с данным id не существует
403 Forbidden Попытка доступа к ресурсу на которого у пользователя нет прав.
409 Conflict Попытка создания дублирующего ресурса, например регистрация пользователя с существующим в БД email'ом.
422 Unprocessable Entity Форма с невалидными данными.
500 Internal Server Error В случае какого-то непредвиденного исключения.

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

 REST HTTP status codes

 Proper use of HTTP status codes in a “validation” server

Руководство по роутингу в Ruby on Rails:

 Rails Routing from the Outside In

Диссертация Роя Филдинга по REST:

Fielding, Roy Thomas. Architectural Styles and the Design of Network-based Software Architectures. Doctoral dissertation, University of California, Irvine, 2000.

Результаты опроса на хабре: