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:
- :internal_server_error (500)
- :not_found (404)
- :unprocessable_entity (403)
Korzysta także z 3 metod pomocniczych:
- error_layout
- template?
- 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.