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