Các cách handle lỗi trong Rails - Cách tiếp cận Modular
Bài đăng này đã không được cập nhật trong 6 năm
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.
All rights reserved