Các cách handle lỗi trong Rails - Cách tiếp cận Modular

Luật Murphy:

Theo luật của Murphy, bất cứ điều gì cũng có thể sai, và nó sẽ sai, đó là lý do tại sao chúng ta nên chuẩn bị cho nó. Nó áp dụng ở mọi nơi, ngay cả trong việc phát triển phần mềm. Ứng dụng chúng ta phát triển phải đủ mạnh để xử lý nó. Nói cách khác, nó phải linh hoạt.

Anything that can go wrong, will go wrong.

— Murphy’s Law

Thường thì trong Rails, chúng ta xử lí mọi thứ ở Controller. Ví dụ bạn đang viết một API bằng cách sử dụng Rails. Hãy em phương thức show của controller sau render user bằng JSON:

# After including ErrorHandler module in ApplicationController
# Remove the Error block from the controller actions.
class UsersController < ApplicationController
  def show
    @user = User.find_by!(id: params[:id])
    render json: @user, status: :ok
  end
end

Khi user được tìm thấy nó sẽ render lại json, chuyện này là bình thường, nhưng nếu nó không tìm thấy user, thì chắc chắn ở đây sẽ trả về lỗi 500. Và chúng ta nên handle trường hợp này để báo cho client biết, thay vì chỉ hiển thị lỗi từ server. Trong Rails còn rất nhiều tình huống gặp các lỗi bất ngờ như thế này, nhất là những lúc dùng create!, update!,...

Exception != Error

Trước khi ta giải quyết lỗi, đương nhiên, chúng ta phải hiểu rõ những gì đang viết, và dự đoán sẽ xảy ra những trường hợp nào. Như đã thấy trong ví dụ trên, ta nhận được lỗi ActiveRecord::RecordNotFound. Và ta sẽ viết 1 đoạn "try-catch" như sau để handle đó:

# This would work perfectly would and handles RecordNotFound error within the block.

begin
  @user = User.find_by!(id: 1)
rescue ActiveRecord::RecordNotFound => e
  print e
end

Nhưng khi bạn muốn xử lí từ tất cả các ngoại lệ thì điều thực sự quan trọng là phải biết được sự khác biệt giữa ngoại lệ và lỗi trong Ruby. Không bao giờ giải cứu từ Exception.It cố gắng để xử lý mọi ngoại lệ duy nhất mà thừa hưởng từ lớp ngoại lệ và cuối cùng dừng việc thực hiện.

# Rescues from all types of Exceptions inside the block

# Rescue an Exception would propagate and handle every class that inherits from Exception and stops the execution.

begin
  @user = User.find_by!(id: 1)
rescue Exception => e # Never do this!
  print e
end

Thay vào đó chúng ta cần phải giải cứu từ StandardError

# Since every error & exception class inherits from StandardError it is sufficient to

# Rescue from StandardError.

begin
  @user = User.find_by!(id: 1)
rescue StandardError => e
  print e
end

The Rescue

Để xử lý các lỗi chúng ta có thể sử dụng khối rescue. Khối giải cứu tương tự như khối try..catch nếu bạn đến từ thế giới Java. Đây là ví dụ tương tự với một khối rescue.

class UsersController < ApplicationController
  def show
    @user = User.find(params[:id])
    render json: @user, status: :ok
  rescue ActiveRecord::RecordNotFound => e
    render json: {
      error: e.to_s
    }, status: :not_found
  end
end

Với cách tiếp cận này các lỗi được giải cứu trong các controller methods. Mặc dù điều này hoạt động hoàn hảo nó có thể không phải là cách tiếp cận tốt nhất để xử lý các lỗi.

Error Handling — Modular Approach

Để xử lý các lỗi ở một nơi lựa chọn đầu tiên của ta sẽ được viết trong dưới ApplicationController. Nhưng cách tốt nhất để tách nó ra khỏi logic ứng dụng.

Chúng ta hãy tạo một mô đun xử lý các lỗi trên phạm vi toàn cầu. Tạo một module ErrorHandler (error_handler.rb) và đặt nó dưới lib/error (hoặc bất cứ nơi nào để tải từ) và sau đó bao gồm nó trong ApplicationController của ta.

Quan trọng: Tải môđun Lỗi trên App startup bằng cách chỉ định nó trong config/application.rb.

class ApplicationController &lt; ActionController::Base

# Prevent CSRF attacks by raising an exception.

# For APIs, you may want to use :null_session instead.

  protect_from_forgery with: :exception
  include Error::ErrorHandler
end

# Error module to Handle errors globally

# include Error::ErrorHandler in application_controller.rb

module Error
  module ErrorHandler
    def self.included(clazz)
      clazz.class_eval do
        rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
      end
    end

    private
    def record_not_found(_e)
      json = Helpers::Render.json(:record_not_found, _e.to_s)
      render json: json, status: 404
    end

  end
end

Lưu ý: Tôi đang sử dụng một vài lớp Helper để hiển thị đầu ra json.

Trước khi tiếp tục với module error_handler ở đây là một bài viết thực sự thú vị về các mô-đun mà bạn chắc chắn nên kiểm tra. Nếu bạn nhận thấy phương pháp self.included trong một mô-đun hoạt động giống như nếu nó được đặt trong lớp ban đầu. Vì vậy, tất cả chúng ta phải làm là bao gồm các module ErrorHandler trong ApplicationController.

class UsersController < ApplicationController
  def show
    @user = User.find(params[:id])
    if @user
      render json: @user, status: :ok
    else
      render json: {
        error: "User with id #{params[:id]} not found."
      }, status: :not_found
    end
  end
end

Hãy refactor các ErrorModule để chứa nhiều khối xử lý lỗi. Nó có vẻ sạch hơn nhiều cách này.

# Refactored ErrorHandler to handle multiple errors

# Rescue StandardError acts as a Fallback mechanism to handle any exception

module Error
  module ErrorHandler
    def self.included(clazz)
      clazz.class_eval do
        rescue_from ActiveRecord::RecordNotFound do |e|
          respond(:record_not_found, 404, e.to_s)
        end
        rescue_from StandardError do |e|
          respond(:standard_error, 500, e.to_s)
        end
      end
    end

    private

    def respond(_error, _status, _message)
      json = Helpers::Render.json(_error, _status, _message)
      render json: json
    end

  end
end

Nếu bạn nhận thấy lỗi ActiveRecord:RecordNotFound cũng kế thừa StandardError. Vì chúng ta có một cơ chế giải cứu cho nó chúng ta có được một: record_not_found. Khung StandardError hoạt động như một cơ chế dự phòng để xử lý tất cả các lỗi.

Define your own Exception.

Chúng ta cũng có thể định nghĩa các lớp Error của chúng ta thừa hưởng từ StandardError. Để giữ mọi thứ đơn giản chúng ta có thể tạo một lớp CustomError giữ các biến chung và các phương thức cho tất cả các lớp lỗi do người dùng xác định. Bây giờ UserDefinedError của chúng tôi mở rộng CustomError.

module Error
  class CustomError < StandardError
    attr_reader :status, :error, :message

    def initialize(_error=nil, _status=nil, _message=nil)
      @error = _error || 422
      @status = _status || :unprocessable_entity
      @message = _message || 'Something went wrong'
    end

    def fetch_json
      Helpers::Render.json(error, message, status)
    end

  end
end
module Error
  class NotVisibleError < CustomError
    def initialize
      super(:you_cant_see_me, 422, 'You can\\'t see me')
    end
  end
end

lib/error/custom_error.rb and lib/error/not_visible_error.rb

Chúng ta có thể ghi đè các phương thức cụ thể cho từng Lỗi. Ví dụ NotVisibleError mở rộng CustomError. Như bạn có thể nhận thấy chúng ta ghi đè lỗi error_message.

module Error
  module ErrorHandler
    def self.included(clazz)
      clazz.class_eval do
        rescue_from CustomError do |e|
          respond(e.error, e.status, e.message)
        end
      end
    end

    private

    def respond(_error, _status, _message)
      json = Helpers::Render.json(_error, _status, _message)
      render json: json
    end

  end
end
class UsersController &lt; ApplicationController
  def show
    @user = User.find_by!(id: params[:id])
    raise Error::NotVisibleError unless @user.is_visible?
    render json: @user, status: :ok
  end
end

NotVisibleError handled in ErrorModule

Để xử lý tất cả các lỗi do người dùng xác định, tất cả những gì chúng ta phải làm là rescue từ CustomError. Chúng tôi cũng có thể rescue từ Lỗi cụ thể nếu chúng ta muốn xử lý nó một cách khác.

404 and 500

Bạn có thể xử lý các ngoại lệ thông thường như 404 và 500, mặc dù nó hoàn toàn phù hợp với nhà phát triển. Chúng ta cần phải tạo một lớp điều khiển riêng biệt, ErrorsController cho nó.

class ErrorsController < ApplicationController
  def not_found
    render json: {
      status: 404,
      error: :not_found,
      message: 'Where did the 403 errors go'
    }, status: 404
  end

  def internal_server_error
    render json: {
      status: 500,
      error: :internal_server_error,
      message: 'Houston we have a problem'
    }, status: 500
  end
end

Để Rails sử dụng Routes để giải quyết các trường hợp ngoại lệ. Chúng ta chỉ cần thêm dòng sau vào application.rb.

config.exceptions_app = routes

Rails.application.routes.draw do
  get '/404', to: 'errors#not_found'
  get '/500', to: 'errors#internal_server_error'

  root 'home#index'
  resources :users, only: [:create, :show]
  get 'not_visible', to: 'home#not_visible'
end

Và bây giờ lỗi 404 sẽ về errors#not_found và lỗi 500 về errors#internal_server_error.

Lời kết

Cách tiếp cận Modular là cách để xử lý lỗi của Rails. Bất cứ khi nào chúng tôi muốn thay đổi một thông báo lỗi cụ thể/định dạng chúng tôi chỉ cần thay đổi nó ở một nơi. Bằng cách tiếp cận này, chúng ta cũng tách biệt logic ứng dụng khỏi xử lý lỗi do đó làm cho Controller gọn hơn.