[Rails] Exception handling #2

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

В ранней статье не описывалась обработка ошибки авторизации. Если пользователь пытается войти без токена, то внутри модуля с этой проверкой, происходит возврат статуса //403// без каких либо дополнительных сообщений:

module ApiHelper  
  def require_auth
    head :forbidden unless current_client
  end

  def current_client
    @client ||= Client.find_by_api_token(params[:api_token])
  end
end  

Но вместе с ним в заголовках вернется и Content-Type, который не всегда должен быть markdown/html.

Content-Type: markdown/html; charset=utf-8\r\nStatus: 403 Forbidden  

Это сломает вашего клиента, который к примеру написан с использованием Weary.

Правильный способ

Есть хорошая статья, в которой говорится, что лучше всего рассказывать пользователю о том, что он неправ через исключения. Т.е. наилучшим сигналом будет не запись в лог проблем, а именно бросание исключения. Логи далеко не все читают, а из тех, кто читает - умеющих его нормально разбирать и понимать очень и очень мало.

Переделаем немного наш модуль:

def require_auth  
  raise ApplicationController::Forbidden unless current_client
end  

В главном ApplicationController пропишем

class ApplicationController < ActionController::Base  
  class Forbidden < StandardError; end
end  

в config/application.rb добавляем:

config.action_dispatch.rescue_responses.merge!(  
  'ApplicationController::Forbidden' => :forbidden
)

routes.rb:

match "/403" => "errors#forbidden"  

Добавляем написанный в предыдущей заметке errors_controller метод forbidden

def forbidden  
  @body_msg = "Access Denied"
  render status: :forbidden
end  

Далее добавляем вью с нужным нам форматом вывода и после запроса получаем:

Content-Type: application/json; charset=utf-8\r\nStatus: 403 Forbidden  
{ "message": "Access Denied" }

То, что нужно! Таким образом можно обработать любую исключительную ситуацию и возвращать разные сообщения и статусы в нужных местах. Просто добавьте исключение, обработчик и пропишите в роутах и application.rb нужное исключение, создайте вью с правильным форматом ответа и радуйтесь жизни)

UPD: в комментариях добавили про 4ые рельсы и матч, вношу поправку, если вы пишите на 4х, то в роутах должно быть: routes.rb:

match "/403" => "errors#forbidden", via: :get  

Прочитать

[Rails] Exception handling

Продолжаю свои увлекательные (или не очень) рассказы про рабочие будни и задачи, которые надо было решать. Сразу к делу:

Задача

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

№ Контекст

Иногда в приложении происходит неведомая х**ня что-то странное, и вылезает исключение, с ответом в виде статичной html страницы и в апи тоже, отчего случается веселье на всех концах.

А что там под капотом?

Под капотом Rack-based приложение на Rails. Rack был изначально придуман, чтобы упростить всем жизнь, он представляет из себя слоеный пирог middleware. Каждый слой это Ruby объект, принимающий в конструктор объект app, отвечающий на метод call, в который прокидывается environment, а возвращается массив(Array), состоящий ровно из трех элементов: status, headers, body. Начиная с версии руби 1.9 body должно реализовывать метод each, т.е. по сути быть тем же массивом.

Middleware является реализацией то ли паттерна Chain of Responsibility, то ли Pipeline, этого момента я сам до конца не понял. С одной стороны, оно как бы отдает на выход то, что принимает следующий слой, с другой, реквест, который пришел, обрабатывают не все слои, а только те, которые активированы или требованиям которых запрос удовлетворяет. Ну как-то так...

А как это делают?

Готовые решения уже есть, но чтобы все самому прочувствовать, надо залезть во внутренности Rails и посмотреть, закатали рукава и вперед.

Чтобы получить весь список middleware из приложения набираем в консоли:

$ rake middleware

Смотрим на понятно-непонятные буковки и нас интересует: use ActionDispatch::ShowExceptions

В нашем приложении используется последняя стабильная сборка 3.2.9, поэтому её и будем копать:

actionpack/lib/actiondispatch/middleware/showexceptions.rb

def call(env)  
  begin
    response = @app.call(env)
  rescue Exception => exception
    raise exception if env['action_dispatch.show_exceptions'] == false
  end
  response || render_exception(env, exception)
end  

Ничего особо сложного, прокидываем дальше запрос и ждем пока вылетит исключение, если такая ситуация не возникла, то просто отдадим response, если что-то пошло не так, то поймаем Exception(т.е. любое исключение), т.к. в конфиге рельсов можно настроить все что хочешь, то и этот middleware можно отключить, если так, то ошибка необработанной уйдет дальше по цепочке. Если слой не отключен, то вызовем метод renderexception//. А вот в нем творятся интересные вещи(код приведу не полностью):

def render_exception(env, exception)  
  wrapper = ExceptionWrapper.new(env, exception)
  ...
  env["PATH_INFO"] = "/#{status}"
  response = @exceptions_app.call(env)
rescue Exception => failsafe_error  
  $stderr.puts "..."
  FAILSAFE_RESPONSE
end  

Во-первых, исключение отдается в ExceptionWrapper, который сомтрит готовый маппинг известных ошибок в символьный аналог статус кодов. Во-вторых, в переменную пути складывается статус-код, что нам впоследствии пригодится, и далее, обработка всего и вся отдается некоему @exceptionsapp_. Это еще что такое?

А это ,всего навсего, еще одно middleware, которое по дефолту в Rails занимается обработкой исключений. Если почитаете гайды, то узнаете, что имя ему PublicExceptions. Все, что там происходит, так это прокидывание статуса(404, 422, 500) дальше по цепочке и рендеринг стандартных страниц с ошибками, которые лежат в public.

А дальше что?

А дальше, сконфигурировать можно все, поэтому переопределим exceptions_app. Тут встает вопрос, писать своё middleware или взять что-то готовое?

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

А какое готовое? Есть ли такое приложение или слой, который органично вписывался бы в существующую структуру и при этом решал поставленную задачу? Да.

В application.rb укажем:

config.exceptions_app = self.routes  

Как вы помните в path_info складывается путь /404, /500, /422 их будем ловить в routes.rb. Перенаправим на обычный контроллер, назовем его errors.

match '/404', :to => "errors#not_found"  
match '/422', :to => "errors#unprocessable_entity"  
match '/500', :to => "errors#internal_error"  

Теперь создадим errors_controller.rb в папке /app/controllers/, никакой авторизации, эти страницы должны быть доступны для всех. Добавляем

respond_to :json, :html

def not_found  
  respond_with({:message => 'Not Found'}, :status => :not_found)
 end

Аналогично определяем еще 2 метода, заменив только статусы на соответствующие ошибкам(422 - :unprocessableentity, 500 - :internalserver_error).

Напишем тесты — ссылка на github. Ничего сложного, код опущу для экономии места.

Далее сделаем view отдельно для html и json и сложим их в app/views/errors/ Последнее, что надо сделать, чтобы в режиме development увидеть кастомные страницы, это заменить значение одного параметра в config/environments/development.rb true -> false.

config.consider_all_requests_local = false  

Полный код можно посмотреть и скачать с github demo_exception_handling. После этого можно сделать bundle install, затем запустить сервер(bundle exec rails s) и обратиться по адресам стандартных страничек ошибок:

Там можно увидеть наши простенькие кастомные страницы, а что самое классное, если обратить к этим страницам с форматом json, то получим

{ message: 'Not Found' }

а не статичный html. Если потребуется можно выводить какие-то специфические коды ошибок в апи и отдавать их в json как будет удобно. В общем простор для фантазии. На этом я думаю все.

Откуда черпал информацию:

Прочитать

Субботний хакатон или как написать фреймворк, который тестирует сам себя

У меня очень интересная работа, и одна из её особенностей это возможность постоянного развития в профессиональном плане. Бывают у нас субботние посиделки, когда люди делятся на пары, причем не кто с кем привык, а в произвольном порядке, и занимаются парным программированием. Дома такого добиться практически невозможно. Скорость прокачки в эти моменты зашкаливает, за 4-5 часов такого веселья можно узнать больше чем за месяц.

В этот раз нам досталось писать фреймворк для тестирования, который бы тестировал сам себя. Когда слышишь такое задание, некоторые шаблоны в голове, как минимум, начинают ломаться. Далее расскажу, последовательно, что и как приходило в голову и воплощалось в код. На все провсе ушло 3 часа, код не претендует на невероятную красоту и лаконичность, но материала нового было предостаточно. Для тех кому не терпится — сразу ссылка на репозиторий в git. (SelfTestingFramework)

Родоначальник этого задания находится здесь и был написан за 1,5 часа в самолете.

Формат Тут все просто, сразу понятно, что это гем. Кстати, уже начинал писать тут, как сделать свой гем с помощью бандлера, но так и не продолжил серию. Начало такое же

bundle gem self_testing_framework

Структура папок и файлов сформирована, дальше по накатанной, инициализируем репозиторий и пушим на github.

С чего начать?

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

А потом неожиданно вспомнили, что мы, вообще-то, работаем по TDD и пишем ни много ни мало фреймворк для тестирования, поэтому правильнее начать с написания тестов!

Поехали!

Создаем папки test/self_testing_framework/, внутри первый и единственный файл с тестами testcase_test.rb. Также положим test_helper.rb в папку test/ в лучших традициях тестирования. Теперь немного покодим:

test/self_testing_framework/testcase_test.rb:

def test_assert_true  
  assert true
end  

В репозитории можно посмотреть полный код всего, что получилось, тут буду просто приводить мысли и некоторые отрывки. На данный момент у нас нет ни класса TestCase, ни метода assert, ни «запускатора» тестов. Ну тогда пойдем и создадим lib/self_testing_framework/test_case.rb и в нем опишем метод assert и run. Метод run у нас будет на уровне класса, а assert сделаем доступным в инстанцированном классе. Напомню, что все наши тесты (целый 1) унаследованы от TestCase. В последствии мы добавили before и after, сделав их пустыми, дабы наследники могли их спокойно переопределять.

lib/self_testing_framework/test_case.rb:

def assert(arg, fail_message = nil)  
  fail_message ||= "Assertion Fails"
  raise AssertFalse.new(fail_message) unless arg
end  

Все просто и весело, если что-то пойдет не так и утверждение окажется неверным, то зарейзим собственную ошибку AssertFalse. Хммм, которой у нас нет. Пойдем её тоже создадим:

lib/self_testing_framework.rb:

class AssertFalse < RuntimeError; end  
autoload "TestCase", "self_testing_framework/test_case"  

Добавили непосредственно в главном файле, откуда виден весь интерфейс гема. Почему унаследовано от RuntimeError, и вообще как правильно работать с исключениями можно узнать из статьи Иерархия ошибок в Ruby из великолепного сборника статей от Kaize. Ну и раз уж зашли в этот головной файл, добавим наш TestCase в пути автолоада.

Сказ о великом «запускаторе»

Но как-то надоело немножко просто так кодить и файлики создавать, хочется уже что-нибудь запустить и посмотреть, что оно вообще живо. Тут встает вопрос: а как? Чуть выше уже описывал варианты с бинарной утилитой для запуска, а также варианты с turn и просто с Ruby. Будем запускать вот так:

ruby -Itest test/self_testing_framework/testcase_test.rb

ОК, добавили папку test в load_path натравили скрипт на файл testcase_test.rb, а дальше то что? Там ничего не запускается, просто класс и все. Пошел research с поиском всевозможных вариантов, в итоге пришли к функции at_exit. Функция, принимающая на вход блок, который затем конвертируется в Proc объект и исполняется, вызывается она после окончания работы программы. Т. е. сразу, после того, как наш файл testcase_test.rb с классом, тестирующим функциональность фреймворка, будет загружен и исполнен, вызовется функция at_exit, в которой мы и будем запускать наши тесты.

lib/self_testing_framework.rb"

at_exit do  
  runner = SelfTestingFramework::Runner.new(SelfTestingFramework::TestCase.descendants)
  runner.execute
end  

Тут стоит обратить внимание на метод descendants, вызванный у класса TestCase и на инициализацию Runner. descendants выдаст нам список всех наследников конкретного класса. Из коробки в Ruby нет такого метода, а появляется он в ActiveSupport, подробнее про это можно прочитать тут. Чтобы этот метод появился в нашем прекрасном фреймворке необходимо сделать 2 простых вещи:

  1. в gemspec добавить зависимость
    gem.add_runtime_dependency «active_support»
  2. сделать require в основном модуле
    require «active_support/core_ext/class/subclasses»

Но откуда у нас появятся там потомки? При запуске конкретного теста:

ruby -Itest test/self_testing_framework/testcase_test.rb

происходит добавление в load_path папки test/ и класс, описанный в файле testcase_test.rb загружается в память, а поскольку наследуется от TestCase, то он попадет в массив descendants. Т. о. в конструктор Runner'а попадут все классы потомки TestCase с тестами, которые на момент окончания работы программы будут описаны. Идем далее и напишем наш «великий» запускатор.

Что вообще нужно? Во-первых, мы закинули в конструктор все классы с тестами и вызвали некий метод execute, поэтому, видимо, у нас будет конструктор, который принимает объект classes и складывает в инстанс переменную.

lib/self_testing_framework/runner.rb:

def initialize(classes)  
  @classes = classes
end  

А в методе execute надо вытащить все тесты, и последовательно их запустить. Для этого нужно получить все методы класса и грепнуть по регулярке /test_/. Тут мы вспоминаем, что было бы неплохо посчитать сколько у нас пройденных, ошибочных и вообще тестов. Для этого надо куда-то результат складывать, а затем выводить отчет. Отсюда появляется следующий код:

lib/self_testing_framework/runner.rb:

def execute  
  reporter = Reporter.new
  test_results = TestResult.new
  @classes.each do |klass|
    klass.new.methods.grep(/test_/).each do |test_method|
      begin
        klass.run test_method
        test_results.passed(klass.name, test_method)
      rescue SelfTestingFramework::AssertFalse => e
        test_results.errored(klass.name, test_method, e.message)
      end
      reporter.report test_results.last_test
    end
  end
  reporter.total_report test_results
end  

В test_result записываются результаты тестирования, а Reporter будет неким универсальным объектом формирования отчетов, на который в последствии можно будет навести красивый вывод и формирования результатов в разном виде. Код простой, поэтому примеры здесь приводить не буду, все можно посмотреть в репозитории. Теперь уже пора наконец-то запустить наш великолепный фреймворк и что-нибудь протестировать, например себя:

ruby -Itest test/self_testing_framework/testcase_test.rb .F

SelfTestingFramework::TestCaseTest#test_assert_false  
  ERROR: Assertion Fails

——————————————————————
Tests: 2, Passed: 1, Errors: 1  

И-и-и-ха! Вот собственно и все. У нас получился маленький простенький фреймворк для тестирования. Написан он был ровно за 3 часа мной и @mstolbov(профиль на github) в рамках субботнего хакатона.

P.S. Можете конечно покидаться чем-нибудь за кривость, но по мне это было крайне познавательно и весело.

Прочитать

[Ruby] Читайте документацию или как красиво работать с many-to-many

Поделюсь решением проблемы, возникшей при выполнении очередной задачи на работе.

Ситуация:

Есть список рассылок List, у него может быть много подписчиков Subscribers (связь многие ко многим), реализовано через третью таблицу SubscriberLists.

class List < ActiveRecord::Base

  has_many :subscriber_lists, :dependent => :destroy
  has_many :subscribers, :through => :subscriber_lists
end  

Требуется:

Выводить списки рассылки в порядке обновления, т. е. те, кто был недавно обновлен должны быть выше. Под обновлением в нашем случае понимается как изменение атрибутов List, так и добавление или удаление подписчиков в список рассылки. Решение: Для вывода списка в апи делаем:

lists = current_client.lists.by_updated_at

Кого смутило by_updated_atсюда. Великолепный гем UsefullScopes от Kaize.

Подпишем желающих(в классе List):

def add_subscriber(subscriber)  
  self.subscribers << subscriber
end  

Вот оно счастье, но тесты мне ответили красным светом. Добавление подписчика вносило изменения лишь во вспомогательную таблицу.

Тут возникла проблема: как выстроить списки в нужном порядке, если без лишних телодвижений обновления не будет.

Держа в голове, что я уже почти 3 месяца как пишу на руби, решил слазить в гугл, а не костылять сразу. Но запросы вида «как мне обновить связанную запись» выдавали исключительно информацию про accepts_nested_attributes_for, что к моей проблеме отношения не имело. Попытав удачу в течение минут 15, решил подойти к проблеме с другой стороны — можно повесить действие на изменение объекта в SubscriberLists:

def after_commit(subscriber_list)  
  subscriber_list.list.update_attributes(:updated_at => Time.zone.now)
end  

У List даем доступ на запись в updated_at

attr_accessible :updated_at

Выглядит монструозно, но работает... Но это же Ruby, но это же Rails, тут должно быть красивое решение! И оно нашлось: http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html (ищем belongs_to и далее :touch)
Решением большинства проблем является фраза: RTFM, и тут прямо в точку! Пишем в SubscriberLists:

belongs_to :list, :touch => true

И все... Теперь, при добавлении подписчика происходит апдейт списка с подписчиками, и он автоматом попадет на первое место в выборке. Читайте доку внимательнее.

Прочитать