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

в 20:59, , рубрики: json, rest, RESTful, ruby on rails, Веб-разработка, Проектирование и рефакторинг, метки: , , ,

Обработка ошибок в 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:

	def 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.

	def 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 В случае какого-то непредвиденного исключения.

Тем не менее вопрос, какие коды использовать в конкретном случае может зависит от многих факторов.
Поэтому привожу полезные обсуждения на стековерфлоу:
Обработка ошибок в RESTful приложениях REST HTTP status codes
Обработка ошибок в RESTful приложениях Proper use of HTTP status codes in a “validation” server

Руководство по роутингу в Ruby on Rails:
Обработка ошибок в RESTful приложениях 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.

Автор: kreshikhin

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js