0

Custom Errors Page in Rails

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ỏ:

  1. 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
  2. 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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí