Custom Errors Page in Rails
Bài đăng này đã không được cập nhật trong 9 năm
Có thể nói đây là bài đầu tiên mình viết về Rails. Dù vấn đề này có thể không mới hay nhiều người đã biết nhưng với một người vẫn còn gà mờ về Rails như mình thì thực sự nó đem lại cho mình rất nhiều cảm xúc lúc tìm hiểu về nó. Vấn đề mình tìm hiểu là custom các trang hiển thị lỗi ở Rails. Các trang lỗi default mà Rails có đây là các lỗi 404, 422, 500 để hiển thị mấy cái lỗi kiểu như không thấy page, không thấy action, không thấy template, không thấy record... Có lẽ bạn cũng chả lạ gì mấy màn hình
hay là
Vậy làm thế nào để thay vì hiển thị mấy trang default này của Rails, web sẽ hiển thị những trang page báo lỗi do mình thiết kế.
Ruby 2.xx
Quay trở lại với ruby 2.x thì đây là vấn đề được xử lý một cách cực kì đơn giản. Bạn chỉ cần sử dụng rescue_from
và được viết như dưới đây ở application_controller.rb
#application_controller.rb
class ApplicationController < ActionController::Base
rescue_from Exception, with: :render_500
rescue_from UnauthorizedException, with: :render_401
rescue_from ActionController::RoutingError, with: :render_404
def render_401
#render custom 401 page
end
def render_404
#render custom 404 page
end
def render_500
#render custom 500 page
end
end
(ok) và đoạn code trên sẽ chỉ chạy ở Rails 2.xx. Lý do là vì ở Rails 2.x thì việc handle cũng như bắt những exception này được xử lý ở ActionController::RoutingError, do đó bạn có thể rescue_from
thoải mái ở đây.
Tuy nhiên từ phiên bản 3. thì việc routing request được xử lý hoàn toàn ở middleware (ActionDispatch), tương đương với việc ActionDispatch sẽ bắt lỗi, render đến page lỗi trước khi mà request này đến được với ActionController nên một vài exception được bắt ở những dòng code bên trên sẽ không chạy từ Rails 3, ví dụ như là ActionController::RoutingError
, ActionController:InvalidAuthenticyToken
... Vậy thì sẽ phải xử lý như thế nào (?)
Rails 3.x, 4.x
Sau một hồi mày mò anh Google, rồi thì tự tìm hiểu thì mình thấy có 4 cách như dưới đây:
Phương án 1
Trong routes.rb
sẽ lấy viết routes get tất cả các request, trỏ đến một hàm ở application_controller.rb
. Và ở hàm này sẽ raise exception. Như vậy thì vẫn sử dụng được rescue_from
. Ví dụ ở dưới đây
#application_controller.rb
class ApplicationController < ActionController::Base
rescue_from Exception, with: :render_500
rescue_from UnauthorizedException, with: :render_401
rescue_from ActionController::RoutingError, with: :render_404
def raise_not_found!
raise ActionController::RoutingError.new("No route matches #{params[:unmatched_route]}")
end
def render_401
#render custom 401 page
end
def render_404
#render custom 404 page
end
def render_500
#render custom 500 page
end
end
và ở trong file routes.rb
viết dòng dưới đây vào cuối file
# -*- coding: utf-8 -*-
Rails.application.routes.draw do
#...
match '*path' => 'application#raise_not_found!', via: [:get, :post]
end
Tuy nhiên cách này là cách khá là sida vì lại có một routes nhận tất cả các request. Chưa kể lại chỉ bắt được đúng một exception cho lỗi 404 là ActionController::RoutingError
. Thế nên đây là cách không được recommend nhất =))
Trước khi đến cách thứ 2 và cách thứ 3 mình sẽ show các bạn thấy dòng code ở trong ActionDispatch để show exception.
# File railties/lib/rails/application/default_middleware_stack.rb
#...
middleware.use ::ActionDispatch::ShowExceptions, show_exceptions_app
#...
def show_exceptions_app
config.exceptions_app || ActionDispatch::PublicExceptions.new(Rails.public_path)
end
Có lẽ đến đây bạn đã hiểu được cách thứ 2 và 3 của mình ) đó là viết một exceptions_app hoặc overide lại method trong class ActionDispatch::PublicExceptions
. Có một điều mình cần lưu ý đó là trước khi bắt đầu thì bạn nên tìm hiểu về Rack.
Phương án 2
Phương án 2 chính là viết một exceptions_app để bắt lỗi và trả về custom page mà mình muốn. Code sẽ như sau:
- Thêm error method vào
application_controller.rb
#application_controller.rb
class ApplicationController < ActionController::Base
rescue_from Exception, with: :render_500
rescue_from UnauthorizedException, with: :render_401
rescue_from ActionController::RoutingError, with: :render_404
def error
raise env["action_dispatch.exception"]
end
def render_401
#render custom 401 page
end
def render_404
#render custom 404 page
end
def render_500
#render custom 500 page
end
end
Tạo một file config/initializers/exception_app.rb
Rails.configuration.exceptions_app =-> (env) {
ApplicationController.action(:error).call(env)
}
Xong!!! Quá đơn giản đúng không nào =)) đơn giản là khai báo để ActionDispatch sử dụng exceptions_app của mình thôi. Tuy nhiên như mình đã nói ở trên, để hiểu rõ thì trước hết bạn nên tìm hiểu về Rack )
Phương án 3
Overide lại class ActionDispatch::PublicExceptions
. Trước tiên chúng ta xem qua 3 method render trong class này.
# File railties/lib/action_dispatch/middleware/public_exceptions.rb
module ActionDispatch
class PublicExceptions
#...
private
def render(status, content_type, body)
format = "to_#{content_type.to_sym}" if content_type
if format && body.respond_to?(format)
render_format(status, content_type, body.public_send(format))
else
render_html(status)
end
end
def render_format(status, content_type, body)
[status, {'Content-Type' => "#{content_type}; charset=#{ActionDispatch::Response.default_charset}",
'Content-Length' => body.bytesize.to_s}, [body]]
end
def render_html(status)
path = "#{public_path}/#{status}.#{I18n.locale}.html"
path = "#{public_path}/#{status}.html" unless (found = File.exist?(path))
if found || File.exist?(path)
render_format(status, 'text/html', File.read(path))
else
[404, { "X-Cascade" => "pass" }, []]
end
end
end
end
Để ý method render_html
dựa vào status để render đến file html tương ứng trong folder public. Đó là các status 401, 422, 500 và các file tương ứng trong public. Vậy đơn giản là bạn overide lại method này, viết switch case tương ứng vs từng status và gọi đến file custom error page tương ứng.
Phương án 4
Đơn giản lắm =)) bạn thích sửa thành như thế nào thì vào thẳng mấy file trong folder public mà sửa =)) (mình đùa đấy)
Đánh giá
Tất nhiên là trong 3 phương án trên thì phương án số 1 là tệ nhất, còn trong 2 phương án số 2 và 3 thì phương án số 2 là tốt nhất. Lý do đơn giản nhất là bạn hãy đọc file
# File railties/lib/action_dispatch/middleware/exception_wrapper.rb
#
module ActionDispatch
class ExceptionWrapper
cattr_accessor :rescue_responses
@@rescue_responses = Hash.new(:internal_server_error)
@@rescue_responses.merge!(
'ActionController::RoutingError' => :not_found,
'AbstractController::ActionNotFound' => :not_found,
'ActionController::MethodNotAllowed' => :method_not_allowed,
'ActionController::UnknownHttpMethod' => :method_not_allowed,
'ActionController::NotImplemented' => :not_implemented,
'ActionController::UnknownFormat' => :not_acceptable,
'ActionController::InvalidAuthenticityToken' => :unprocessable_entity,
'ActionController::InvalidCrossOriginRequest' => :unprocessable_entity,
'ActionDispatch::ParamsParser::ParseError' => :bad_request,
'ActionController::BadRequest' => :bad_request,
'ActionController::ParameterMissing' => :bad_request,
'Rack::Utils::ParameterTypeError' => :bad_request,
'Rack::Utils::InvalidParameterError' => :bad_request
)
cattr_accessor :rescue_templates
@@rescue_templates = Hash.new('diagnostics')
@@rescue_templates.merge!(
'ActionView::MissingTemplate' => 'missing_template',
'ActionController::RoutingError' => 'routing_error',
'AbstractController::ActionNotFound' => 'unknown_action',
'ActionView::Template::Error' => 'template_error'
)
#...
end
end
Những exeptions được khai bao bên trên là toàn bộ những default exception của ActionPatch. Có nghĩa là muốn bắt 1 vài exception như là ActiveRecord::NotFound
... thì bạn lại phải viết trong application_controller.rb
dùng rescue_from
, khá là rắc rối. Trong khi đó dùng cách số 2 thì mình có thể khai báo được 1 loạt exception.
(ok) bài viết của mình đã xong. Mình chỉ có 2 chú ý nhỏ:
- Tìm hiểu về Rack để hiểu thêm về Rails. Mình khá thích bài viết ở link sau đây: http://blog.siami.fr/blog.siami.fr/diving-in-rails-the-request-handling
- Tất nhiên là bạn chỉ muốn show custom error page ở staging vs production, còn development thì không để còn biết lỗi chỗ nào =)) vậy thì hãy sử dụng flag
config.consider_all_requests_local
. Và ở controller thì code sẽ như sau
unless Rails.application.config.consider_all_requests_local
rescue_from Exception, with: :render_500
rescue_from ActionController::UnknownAction, with: :render_404
rescue_from ActiveRecord::RecordNotFound, with: :render_404
end
Trên đây là toàn bộ bài viết của mình. Chắc vẫn còn nhiều sai sót, rất mong comment từ các bạn để mình hoàn thiện hơn )
All rights reserved