我们的exception_handler gem 可用于 Ruby on Rails 自定义错误页面。
工作原理
所有 Ruby on Rails 异常都使用config.exceptions_app 处理。这是在 config/application.rb 或 config/environments/*.rb 文件中分配的 - 它需要是回调:
config.exceptions_app 设置异常发生时 ShowException 中间件调用的异常应用程序。默认为 ActionDispatch::PublicExceptions.new(Rails.public_path)。
当 Ruby on Rails 遇到错误时,它会调用 ShowExceptions 中间件。这会调用exception_app 并将整个request(包括exception)发送到exceptions_app:
exceptions_app 需要deliver a response。如果没有,则加载 failsafe:
# show_exceptions.rb#L38
def render_exception(env, exception)
wrapper = ExceptionWrapper.new(env, exception)
status = wrapper.status_code
env["action_dispatch.exception"] = wrapper.exception
env["PATH_INFO"] = "/#{status}"
response = @exceptions_app.call(request.env) # => exceptions_app callback
response[1]["X-Cascade"] == "pass" ? pass_response(status) : response
rescue Exception => failsafe_error # => raised if exceptions_app false
$stderr.puts "Error during failsafe response: #{failsafe_error}\n #{failsafe_error.backtrace * "\n "}"
FAILSAFE_RESPONSE
end
failsafe 存储为FAILSAFE_RESPONSE,位于ShowExceptions 的顶部。
自定义错误页面
如果您想创建自定义错误页面,您需要将自己的回调注入config.exceptions_app。这可以在应用程序中完成,也可以通过 gem 完成:
注意 call 方法是如何使用的——这就是回调的工作方式。 Ruby on Rails (env) 在收到来自 Internet 的请求时被调用;当引发异常时,env 被传递给 exceptions_app。
您的异常处理质量将取决于您如何管理env。这个很重要;引用self.routes 不会推动环境向前发展。
最好的方法是使用单独的控制器来处理异常。这使您可以像处理另一个视图一样处理请求,授予对 layout 和其他组件 (model / email) 的访问权限。
有两种方式处理异常:
- 覆盖
404 / 500 路由
- 调用控制器
我们的 gem 是围绕我们的 controller 设计的 - 每次引发 exception 时都会调用。这让完全控制异常过程,允许100% 品牌化布局。它在 Ruby on Rails 5 上 100% 运行。
管理 Ruby on Rails 的异常
如果你对 gem 不感兴趣,让我解释一下过程:
所有 Ruby on Rails 异常都通过 config.exceptions_app 回调处理。这是在 config/application.rb 或 config/environments/*.rb 文件中分配的 - 它需要是一个回调:
config.exceptions_app 设置异常发生时 ShowException 中间件调用的异常应用程序。默认为 ActionDispatch::PublicExceptions.new(Rails.public_path)。
每当您的应用引发异常时,都会调用 ShowExceptions 中间件。该中间件将异常构建到request 中并将其转发到config.exceptions_app 回调。
默认情况下,config.exceptions_app 指向路由。这就是为什么 Rails 在 public 文件夹中带有 404.html、500.html 和 422.html 的原因。
如果您想创建自定义异常页面,您需要覆盖config.exceptions_app 回调 - 将错误请求传递给适当的处理程序,无论是controller 还是route:
[ middleware ]
有效管理此问题的两种方法是将错误请求发送到路由,或调用控制器。
最简单且最常见的方式是将请求转发到路由;不幸的是,这会忽略请求并阻止您正确详细说明异常。
最好的方法是调用一个单独的控制器。这将允许您传递整个请求,允许您保存它、通过电子邮件发送它或执行许多其他操作。
400 / 500 错误
Rails 只能响应 HTTP 有效错误。
虽然应用的异常可能不同,但返回的状态码应该是40x或50x。这符合 HTTP 规范并概述了 here。
这意味着无论您使用/构建何种异常处理解决方案,Ruby on Rails需要向浏览器返回 40x 或 50x 错误。
换句话说,自定义错误页面与异常的类型几乎没有关系 - 更多的是您如何捕获和提供浏览器响应。
默认情况下,Ruby on Rails 对 public 文件夹中的 404.html、422.html 和 500.html 文件执行此操作。如果您想自己处理异常流,则需要删除这些文件并将错误请求引导到您自己的exceptions_app 回调。
这可以通过routes 或controller(我现在将解释)来完成:
1。路线
最简单的方法是让路由处理。
此方法过于臃肿,需要使用多个操作。管理响应也很困难。
这显示了如何直接用路由替换exceptions_app:
# config/application.rb
config.exceptions_app = self.routes
这是我的代码(Ruby 2.0.0 和 Ruby on Rails 4.0):
应用配置
#config/application.rb
config.exceptions_app = self.routes
路线
#config/routes.rb
if Rails.env.production?
get '404', to: 'application#page_not_found'
get '422', to: 'application#server_error'
get '500', to: 'application#server_error'
end
应用程序控制器
#controllers/application_controller.rb
def page_not_found
respond_to do |format|
format.html { render template: 'errors/not_found_error', layout: 'layouts/application', status: 404 }
format.all { render nothing: true, status: 404 }
end
end
def server_error
respond_to do |format|
format.html { render template: 'errors/internal_server_error', layout: 'layouts/error', status: 500 }
format.all { render nothing: true, status: 500}
end
end
错误布局(完全静态——仅用于服务器错误)
#views/layouts/error.html.erb
<!DOCTYPE html>
<html>
<head>
<title><%= action_name.titleize %> :: <%= site_name %></title>
<%= csrf_meta_tags %>
<style>
body {
background: #fff;
font-family: Helvetica, Arial, Sans-Serif;
font-size: 14px;
}
.error_container {
display: block;
margin: auto;
margin: 10% auto 0 auto;
width: 40%;
}
.error_container .error {
display: block;
text-align: center;
}
.error_container .error img {
display: block;
margin: 0 auto 25px auto;
}
.error_container .message strong {
font-weight: bold;
color: #f00;
}
</style>
</head>
<body>
<div class="error_container">
<%= yield %>
</div>
</body>
</html>
错误视图
#views/errors/not_found_error.html.erb
<div class="error">
<h2>Sorry, this page has moved, or doesn't exist!</h2>
</div>
#views/errors/internal_server_error.html.erb
<div class="error">
<div class="message">
<strong>Error!</strong>
We're sorry, but our server is experiencing problems :(
</div>
</div>
虽然许多人更喜欢“路由”方法,因为它很简单,但它既不高效也不模块化。事实上,如果您的应用程序有任何面向对象的外观,您很快就会将其视为 hack。
一个更多更响亮的方法是使用自定义控制器来捕获纯异常。这样,您可以根据应用程序的整体结构构建流程:
2。控制器
另一种选择是将所有请求路由到控制器。
这是无限强大的,因为它允许您接受请求(异常)并将其传递给视图,同时在后端进行管理。这将允许将其保存到数据库中。
这个gist 展示了如何。
这意味着我们可以挂钩到中间件并将整个请求传递给控制器。
如果此控制器由模型和视图支持,我们可以将其提取到 gem 中(这就是我们所做的)。如果您想手动操作,方法如下:
配置
这种方法的美妙之处在于它直接挂钩到config.exceptions_app。这意味着任何异常都可以本地处理,从而提高效率。为确保此功能有效,您需要将以下代码放入 config/application.rb(exceptions_app 仅适用于 production - development 显示错误):
#config/application.rb
config.exceptions_app = ->(env) { ExceptionController.action(:show).call(env) }
要进行测试,您可以将“本地”请求设置为 false:
#config/environments/development.rb
config.consider_all_requests_local = false # true
控制器
下一步是添加一个exception 控制器。虽然这可以在application_controller 中处理,但最好提取到它自己的中。注意来自application.rb的电话——ExceptionController.action(:show):
#app/controllers/exception_controller.rb
class ExceptionController < ApplicationController
#Response
respond_to :html, :xml, :json
#Dependencies
before_action :status
#Layout
layout :layout_status
####################
# Action #
####################
#Show
def show
respond_with status: @status
end
####################
# Dependencies #
####################
protected
#Info
def status
@exception = env['action_dispatch.exception']
@status = ActionDispatch::ExceptionWrapper.new(env, @exception).status_code
@response = ActionDispatch::ExceptionWrapper.rescue_responses[@exception.class.name]
end
#Format
def details
@details ||= {}.tap do |h|
I18n.with_options scope: [:exception, :show, @response], exception_name: @exception.class.name, exception_message: @exception.message do |i18n|
h[:name] = i18n.t "#{@exception.class.name.underscore}.title", default: i18n.t(:title, default: @exception.class.name)
h[:message] = i18n.t "#{@exception.class.name.underscore}.description", default: i18n.t(:description, default: @exception.message)
end
end
end
helper_method :details
####################
# Layout #
####################
private
#Layout
def layout_status
@status.to_s == "404" ? "application" : "error"
end
end
观看次数
有两个视图可以添加以使其正常工作。
第一个是exception/show 视图,第二个是layouts/error。第一个是给exception_contoller#show一个视图,第二个是给500内部服务器错误。
#app/views/exception/show.html.erb
<h1><%= details[:name] %></h1>
<p><%= details[:message] %></p>
#app/views/layouts/error.html.erb (for 500 internal server errors)
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>Error</title>
<style>
html {
height: 100%;
background: #fff;
}
body {
font-family: Helvetica, Arial, Sans-Serif;
font-size: 14px;
}
.error_container {
display: block;
margin: auto;
margin: 10% auto 0 auto;
width: 40%;
}
.error_container .error {
display: block;
text-align: center;
}
.error_container .error img {
display: block;
margin: 0 auto 15px auto;
}
.error_container .message > * {
display: block;
}
.error_container .message strong {
font-weight: bold;
color: #f00;
}
</style>
</head>
<body>
<div class="error_container"><%= yield %></div>
</body>
</html>
结论
异常并不像错误代码那么重要。
当 Ruby on Rails 引发异常时,它会分配上述 HTTP 响应代码之一。这些允许您的浏览器确定请求是否成功。
在处理异常时,您需要确保能够处理 40* 错误(通常使用与应用程序其余部分相同的布局)和 50* 错误(需要自己的布局)。
在这两种情况下,您最好使用单独的 exception 控制器,这将允许您将 exception 作为对象进行管理。