生產環境 Rails 應用需統一處理未捕獲例外:顯示友善錯誤頁面、記錄詳細日誌、發送郵件通知開發團隊。本文展示在 ApplicationController 使用 rescue_from 分類處理 ActiveRecord、路由、權限、模板等異常,並整合郵件通知與動態路由捕獲 404。
class ApplicationController < ActionController::Base
# 例外分類常數
AR_ERROR_CLASSES = [
ActiveRecord::RecordNotFound,
ActiveRecord::StatementInvalid
].freeze
ERROR_CLASSES = [
NameError, NoMethodError, RuntimeError,
ActionView::TemplateError,
ActiveRecord::StaleObjectError,
ActionController::RoutingError,
ActionController::UnknownController,
AbstractController::ActionNotFound,
ActionController::MethodNotAllowed,
ActionController::InvalidAuthenticityToken
].freeze
ACCESS_DENIED_CLASSES = [CanCan::AccessDenied].freeze
# 僅生產環境啟用統一處理 (開發環境保留詳細錯誤頁)
if Rails.env.production?
rescue_from *AR_ERROR_CLASSES, with: :render_ar_error
rescue_from *ERROR_CLASSES, with: :render_error
rescue_from *ACCESS_DENIED_CLASSES, with: :render_access_denied
end
# 捕獲未匹配路由 (需配合 routes.rb 設定)
def raise_not_found!
raise ActionController::RoutingError.new("No route matches #{params[:unmatched_route]}")
end
# === 處理器實作 ===
def render_ar_error(exception)
handle_exception(exception, 'ActiveRecord')
end
def render_error(exception)
handle_exception(exception, 'Application')
end
def render_access_denied(exception)
handle_exception(exception, 'AccessDenied')
end
private
def handle_exception(exception, exception_class)
# 1. 記錄詳細日誌 (Rails.logger 自動包含回溯)
Rails.logger.error "[#{exception_class}] #{exception.class}: #{exception.message}"
Rails.logger.error exception.backtrace.join("\n")
# 2. 發送郵件通知 (非同步推薦用 ActiveJob)
ExceptionNotifierMailer.notify(exception, exception_class, request).deliver_later
# 3. 渲染對應錯誤頁面
case exception_class
when 'ActiveRecord'
render template: 'errors/ar_error', status: :not_found
when 'AccessDenied'
render template: 'errors/access_denied', status: :forbidden
else
render template: 'errors/internal_error', status: :internal_server_error
end
end
end
# config/routes.rb
Rails.application.routes.draw do
# ... 正常路由 ...
# Rails 3/4: 最後捕獲所有未匹配路由
# Rails 3: match '*unmatched_route', to: 'application#raise_not_found!', via: :all
# Rails 4/5/6/7:
get '*unmatched_route', to: 'application#raise_not_found!'
# 或 match '*unmatched_route', to: 'application#raise_not_found!', via: :all
end
# app/mailers/exception_notifier_mailer.rb
class ExceptionNotifierMailer < ApplicationMailer
default to: -> { Rails.application.config.exception_recipients },
from: 'noreply@myapp.com'
def notify(exception, exception_class, request)
@exception = exception
@exception_class = exception_class
@request = request
@timestamp = Time.current
@host = request&.base_url
@user = current_user # 若有認證系統
mail(subject: "[ERROR][#{Rails.env}] #{exception_class}: #{exception.class}")
end
end
# app/views/exception_notifier_mailer/notify.html.erb
Exception Details
Class: <%= @exception_class %> (<%= @exception.class %>)
Message: <%= @exception.message %>
Time: <%= @timestamp %>
URL: <%= @request&.original_url %>
Method: <%= @request&.request_method %>
Params: <%= @request&.params&.inspect %>
User: <%= @user&.inspect %>
Backtrace
<%= @exception.backtrace.join("\n") %>
資料未找到 (404)
請求的資源不存在或已被移除。
<%= link_to '返回首頁', root_path %>
權限不足 (403)
您沒有權限存取此資源。
系統錯誤 (500)
伺服器發生錯誤,我們已收到通知並將盡快修復。
<%= link_to '返回首頁', root_path %>
手寫郵件通知易遺漏細節,推薦使用成熟 Gem:
# Gemfile
gem 'exception_notification'
# config/initializers/exception_notification.rb
Rails.application.config.middleware.use ExceptionNotification::Rack,
email: {
delimiter: "\n",
sender_address: %{"Error Notifier" },
exception_recipients: %w{dev@myapp.com ops@myapp.com},
email_prefix: "[ERROR][#{Rails.env}] ",
sections: %w{request session environment backtrace}
},
slack: {
webhook_url: ENV['SLACK_WEBHOOK_URL'],
channel: '#exceptions',
additional_parameters: { icon_emoji: ':fire:' }
}
| 實踐 | 說明 |
|---|---|
| 環境區分 | if Rails.env.production? 僅生產環境攔截,開發/測試保留詳細錯誤頁 |
| 例外分類 | 依業務語意分群 (AR、權限、路由、通用),對應不同 HTTP 狀態碼與頁面 |
| 非同步通知 | deliver_later (ActiveJob) 避免阻塞回應,郵件/Slack/Webhook 並行發送 |
| 資訊豐富度 | 郵件包含:請求 URL/參數/Session/用戶/完整回溯/環境變數 |
| 避免洩漏 | 錯誤頁面不顯示堆疊/參數/內部結構,僅友善提示 |
| 監控整合 | 同時送至 Sentry/Rollbar/Datadog/ELK 進行聚合分析、告警去重 |
rescue_from 繼承鏈:子 Controller 可覆蓋父類處理器,順序為「最近定義優先」*unmatched_route 必須置於 路由檔案最末尾render json: {error: 'Not Found'}, status: :not_found原文標題錯誤提示:原標題為「java解析xml之jdom解析xml示例分享」,但實際內容為 Rails 例外處理與錯誤通知。本文已依實際內容重寫。