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

Тем не менее вопрос, какие коды использовать в конкретном случае может зависит от многих факторов.
Поэтому привожу полезные обсуждения на стековерфлоу:
 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.

Как вы реализуете обработку ошибок в веб-приложении?

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Никто ещё не голосовал. Воздержавшихся нет.

ссылка на оригинал статьи http://habrahabr.ru/post/208404/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *