Uwaga! – kod ten działa TYLKO dla Railsów3.
Opis “starego” routingu błędów dostępny tutaj.

Migrując Susanoo na Rails 3.0.0 okazało się że poprzez zmiany w middleware i obsługę błędów w inny sposób – mój system zarządzania renderowaniem błędów nie działa.Szczerze mówiąc niezbyt mnie to martwiło, ponieważ nie był on “aż tak” fajny. Korzystając z okazji (czyt. nie mając wyjścia) zacząłem googlać za sposobami rozwiązania tego problemu. Metody takie jak

rescue_action_in_public
rescue_action_locally

przestały działać.Pierwszą myślą jaka mi się nasunęła, było obsługiwanie błędów przerzucając “złe trafienia” po routsach do kontrolera błędów

match ':action', :to => "errors#index"
match '*anything', :to => "errors#index"

niestety rozwiązanie takie miało jedną wielką wadę – nie obsługiwało internal server error’ów. Błędy 404 mogłem za jego pomocą obsługiwać, jednak w wypadku wystąpienia 500 – nic z tego. Dodatkowo takie rozwiązanie nie działało w wypadku zgłoszenia wyjątkuActiveRecord::RecordNotFound.Nie było więc wyjścia ;) trzeba było napisać własny plugin. Poniżej opiszę jak działa, a tymczasem dla niecierpliwych link do githuba:
http://github.com/mensfeld/custom_errors_handler.

W README jest dokładnie opisane jak go wykorzystać, jednak tutaj wspomnę dla pewności:

  • wrzucamy dop vendor/plugin
  • tworzymy 404.erb, 500.erb w views danego kontrolera/modułu lub w views kontrolera/modułu ale w subkatalogu “layouts”
  • reset serwera
  • “sztuczne” bądź też prawdziwe wygenerowanie 404/500
  • ?
  • profit ;)

A teraz omówienie pluginu.

Cała zabawa w utworzenie tego typu pluginu, polegała na “przejęciu” błędów z ActionDispatch::ShowExceptions. Można to było zrobić, nadpisując metodę render_exception.

Jak widać poniżej, sam kod nie jest wielki. Przechwytuje on błędy i przekazuje je do kontrolera CustomErrorsHandler gdzie nastąpi rozpoznanie błędu i wyrenderowanie odpowiedniego szablonu.

module ActionDispatch
  class ShowExceptions
    private
      def render_exception_with_template(env, exception)
        body = CustomErrorsHandlerController.action(rescue_responses[exception.class.name]).call(env)
        log_error(exception)
        body
      rescue
        render_exception_without_template(env, exception)
      end

      alias_method_chain :render_exception, :template
  end
end

Skoro mamy już przekazywanie błędów, przejdźmy do kontrolera który się tym zajmuje. Jest to CustomErrorsHandlerController.

Obsługuje on 3 rodzaje błędów:

  1. :internal_server_error (500)
  2. :not_found (404)
  3. :unprocessable_entity (403)

Korzysta także z 3 metod pomocniczych:

  1. error_layout
  2. template?
  3. translate_error

Zanim opiszę kod odpowiedzialny za działanie, opiszę każdą z tych metod.

Pierwszą będzie translate_error:

  def translate_error(e)
    case e
    when :internal_server_error then '500'
    when :not_found then '404'
    when :unprocessable_entity then '500'
    else '500'
    end
  end

Metoda ta zwraca “liczbowy” odpowiednik danego błędu. Potrzebujemy tego, ponieważ w naszych szablonach nie chcemy używać szablonuinternal_server_error.erb ale raczej 500.erb.

Druga czyli template? zwraca nam informację czy dany plik szablonu istnieje. Ścieżkę podajemy zaczynając od widoków, czyli nie app/views/jakis_kontroler/, ale /jakis_kontroler/. Kod tej metody jest dość prosty:

  def template?(template)
    FileTest.exist?(File.join(Rails.root, 'app', 'views', "#{template}.erb"))
  end

Ostatnią metodą jest error_layout. Odpowiada ona za znalezienie szablonu błędu, przeszukując strukturę katalogów, zaczynając od ścieżki “najgłębszej”, stopniowo idąc wyżej w hierarchii katalogów.

  def error_layout(path, e)
    e = <strong>translate_error</strong>(e)
    path= path.split('/')
    path.size.downto(0) do |i|
      VALID_ERRORS_SUBDIRS.each { |lay_path|
        template_path = File.join((path[0,i]).join('/'), lay_path, e)
        return template_path if template?(template_path)
      }
      template_path = File.join(path[0,i], e)
      return template_path if template?(template_path)
    end
    e
  end

Metoda ta przyjmuje dwa argumenty – pierwszym z nich jest path. Path jest ścieżką skąd wywołany był błąd. Czyli przykładowo: jeśli błąd wystąpił w kontrolerze MyTest w module Samples (Samples::MyTest), w akcji index, to będzie następujący:

/samples/my_test/index

Parametr e jest to nazwa błędu, którą następnie tłumaczymy naszą metodą translate_error.

Mając ścieżkę rozbijamy ją tak, aby mieć jej kolejne “stopnie”.

    path= path.split('/')

Następnie “przelatujemy” całą strukturę sprawdzając takie katalogi (w kolejności):

/samples/my_test/index/layouts
/samples/my_test/index
/samples/my_test/layouts
/samples/my_test/
/samples/layouts/
/samples/
layouts

Skąd się wzięło “layouts”? Otóż w stałej VALID_ERRORS_SUBDIRS mamy zdefiniowane podkatalogi katalogów które sprawdzamy, tak by sprawdzić także je. Dzięki temu, layouty błędów możemy umieszczać w podkatalogach layouts danych kontrolerów/modułów, nie zaś w katalogach głównych.

Zdefiniowane jest to jako stała, ponieważ może ktoś z Was zapragnie mieć inne podkatalogi na błędy (np podkatalog errors). Wtedy wystarczy sobie takowe dopisać.

Następnie łączymy ścieżkę oraz kod błędu i sprawdzamy metodą template? czy taki szablon istnieje. Odpowiada za to, ten fragment kodu:

      VALID_ERRORS_SUBDIRS.each { |lay_path|
        template_path = File.join((path[0,i]).join('/'), lay_path, e)
        return template_path if template?(template_path)
      }
      template_path = File.join(path[0,i], e)
      return template_path if template?(template_path)

Na samym końcu, jeśli żaden z naszych “dynamicznych” 404/500 nie został znaleziony, zwracamy standardowy 404/500. Skutkuje to wyrenderowaniem layoutu znajdującego się w /public.

To by było na tyle. Jeśli chodzi o testy do tego pluginu, to nie bardzo wiem jak przetestować ActionDispatch::ShowExceptions. Jeśli ktoś z Was to potrafi, prosiłbym o kontakt.