1000 多个 Ruby on Rails 项目中的十大错误(以及如何避免它们)
7. ActionController::ParameterMissing
8. ActionView::Template::Error:未定义的局部变量或方法
结论
为了回馈开发者社区,我们在 Rollbar 查看了数千个项目的数据库,并找出了 Ruby on Rails 项目中最常见的 10 个错误。我们将向您展示导致这些错误的原因以及如何避免它们发生。如果您能避免这些“陷阱”,那么您将成为一名更优秀的开发者。
数据为王,我们收集、分析并排序了Ruby on Rails 应用程序中的十大Ruby 错误。Rollbar收集每个项目的所有错误,并汇总每个错误的发生次数。我们根据指纹对错误进行分组。基本上,如果第二个错误只是第一个错误的重复,我们会将其归为一类。这为用户提供了清晰的概览,而不是像日志文件中那样,看到大量的转储信息。
我们专注于最有可能影响您和您的用户的错误。为此,我们根据不同公司遇到这些错误的项目数量对错误进行了排序。我们特意关注项目数量,以避免大量客户的错误充斥数据集,而这些错误与大多数读者无关。
以下是 10 个最常见的 Rails 错误:
你可能已经注意到了一些熟悉的面孔。让我们深入研究一下这些错误,看看在你的生产应用程序中可能导致这些错误的原因是什么。
我们将提供基于 Rails 5 的示例解决方案,但如果您仍在使用 Rails 4,它们应该会为您指明正确的方向。
1. ActionController::RoutingError
我们先从 Web 应用中常见的一个问题开始,那就是 Rails 版的 404 错误。这ActionController::RoutingError
意味着用户请求了应用中不存在的 URL。Rails 会记录这个错误,虽然看起来像是一个错误,但大多数情况下这并不是应用本身的问题。
这可能是由于指向或来自您应用程序内部的错误链接造成的。也可能是恶意用户或机器人在测试您的应用程序是否存在常见漏洞。如果是这种情况,您可能会在日志中发现类似以下内容:
ActionController::RoutingError (No route matches [GET] "/wp-admin"):
有一个常见原因可能会导致你收到错误信息,ActionController::RoutingError
该错误是由你的应用程序而不是用户操作引起的:如果你将应用程序部署到 Heroku 或任何不允许提供静态文件的平台,那么你可能会发现 CSS 和 JavaScript 无法加载。如果是这种情况,错误信息将如下所示:
ActionController::RoutingError (No route matches [GET] "/assets/application-eff78fd93759795a7be3aa21209b0bd2.css"):
要解决此问题并允许 Rails 提供静态资产,您需要在应用程序的config/environments/production.rb
文件中添加一行:
Rails.application.configure do
# other config
config.public_file_server.enabled = true
end
如果您不想记录由此导致的 404 错误,ActionController::RoutingError
可以通过设置一个 catch all 路由并自行处理 404 错误来避免这种情况。此方法是 lograge 项目建议的。为此,请在文件底部添加以下内容config/routes.rb
:
Rails.application.routes.draw do
# all your other routes
match '*unmatched', to: 'application#route_not_found', via: :all
end
然后将该route_not_found
方法添加到您的ApplicationController
:
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
def route_not_found
render file: Rails.public_path.join('404.html'), status: :not_found, layout: false
end
end
在实现此功能之前,您应该考虑了解 404 错误是否对您很重要。您还应该记住,在应用程序加载后挂载的任何路由或引擎都将无法访问,因为它们将被 catch all 路由捕获。
2. NoMethodError:对 nil:NilClass 未定义方法‘[]’
这意味着您正在使用方括号表示法从对象中读取属性,但该对象缺失或nil
,因此它不支持此方法。由于我们使用方括号,因此很可能我们正在挖掘哈希值或数组来访问属性,而在此过程中缺少了某些内容。当您从 JSON API 或 CSV 文件解析和提取数据,或者只是从控制器操作中的嵌套参数中获取数据时,可能会发生这种情况。
假设用户通过表单提交地址详细信息。您可能希望参数如下所示:
{ user: { address: { street: '123 Fake Street', town: 'Faketon', postcode: '12345' } } }
然后,您可以通过拨打电话进入该街道params[:user][:address][:street]
。如果没有传递地址,params[:user][:address]
则为,nil
而拨打电话[:street]
则会引发NoMethodError
。
您可以对每个参数执行 nil 检查并使用&&
运算符提前返回,如下所示:
street = params[:user] && params[:user][:address] && params[:user][:address][:street]
虽然这样就可以了,但幸运的是,现在有一种更好的方法来访问哈希、数组和事件对象中的嵌套元素,例如ActionController::Parameters
。从 Ruby 2.3 开始,哈希、数组和事件对象都ActionController::Parameters
拥有dig
方法. ,dig
允许你提供要检索的对象的路径。如果在任何阶段nil
返回 ,则dig
返回nil
而不会抛出NoMethodError
。要从上述参数中获取街道,你可以调用:
street = params.dig(:user, :address, :street)
您不会因此收到任何错误,但您需要知道street
可能仍然存在nil
。
另外,如果你也使用点符号来挖掘嵌套对象,那么在 Ruby 2.3 中,你也可以使用安全导航运算符安全地执行此操作。因此,与其调用
street = user.address.street
并得到一个NoMethodError: undefined method street
,nil:NilClass
你现在可以调用。
street = user&.address&.street
上述操作现在与使用 的作用相同dig
。如果地址为 ,nil
则street
将是,并且您在稍后引用 时nil
需要处理。如果所有对象都存在,将被正确分配。nil
street
street
虽然这可以抑制向用户显示错误,但如果它仍然影响用户体验,您可能需要创建一个内部错误来在日志或错误跟踪系统(如 Rollbar)中跟踪,以便您可以看到问题并进行修复。
如果您没有使用 Ruby 2.3 或更高版本,您可以使用ruby_dig gem和ActiveSupporttry
实现与上述相同的效果。
3. ActionController::InvalidAuthenticityToken
我们列表中的第 3 项需要仔细考虑,因为它与我们应用程序的安全性有关。ActionController::InvalidAuthenticityToken
当 POST、PUT、PATCH 或 DELETE 请求丢失或具有不正确的 CSRF(跨站点请求伪造)令牌时将引发。
CSRF 是 Web 应用程序中潜在的漏洞,恶意网站会冒充不知情的用户向您的应用程序发出请求。如果用户已登录,其会话 Cookie 将随请求一起发送,攻击者便可以冒充用户执行命令。
Rails 通过在所有表单中包含一个安全令牌来缓解 CSRF 攻击,该令牌由网站已知并验证,但第三方无法获取。这由我们熟悉的ApplicationController
一行代码
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
end
因此,如果您的生产应用程序出现ActionController::InvalidAuthenticityToken
错误,则可能意味着攻击者正在瞄准您网站的用户,但 Rails 安全措施可以保证您的安全。
不过,还有其他原因可能导致您无意中收到此错误。
阿贾克斯
例如,如果您从前端发出 Ajax 请求,则需要确保在请求中包含 CSRF 令牌。如果您使用 jQuery 和内置的Rails 非侵入式脚本适配器,那么这些已经为您处理好了。如果您想以其他方式处理 Ajax,例如使用Fetch API,则需要确保包含 CSRF 令牌。无论使用哪种方法,您都需要确保您的应用程序布局在<head>
文档中包含 CSRF 元标记:
<%= csrf_meta_tags %>
这将打印出如下所示的元标记:
<meta name='csrf-token' content='THE-TOKEN'>
在进行Ajax请求的时候,读取meta标签内容,并作为header添加到headers中X-CSRF-Token
。
const csrfToken = document.querySelector('[name="csrf-token"]').getAttribute('content');
fetch('/posts', {
method: 'POST',
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json'
'X-CSRF-Token': csrfToken
}
).then(function(response) {
// handle response
});
Webhook/API
有时关闭 CSRF 保护是有充分理由的。如果您预计应用程序中某些 URL 会收到来自第三方的 POST 请求,那么您肯定不希望因为 CSRF 而阻止这些请求。如果您正在为第三方开发者构建 API,或者您希望接收来自某个服务的 Webhook,那么您可能就需要关闭 CSRF 保护。
您可以关闭 CSRF 保护,但请确保将您知道不需要此类保护的端点列入白名单。您可以在控制器中通过跳过身份验证来实现此目的:
class ApiController < ApplicationController
skip_before_action :verify_authenticity_token
end
如果您正在接受传入的 webhook,您应该能够验证请求是否来自受信任的来源,而不是验证 CSRF 令牌。
4. Net::ReadTimeout
Net::ReadTimeout
当 Ruby 从套接字读取数据的时间超过该值(默认值为 60 秒)时,会引发此read_timeout
错误。如果您使用Net::HTTP
、open-uri
或HTTParty
发出 HTTP 请求,则可能会引发此错误。
值得注意的是,这并不意味着如果请求本身花费的时间超过该值就会抛出错误read_timeout
,只是如果某个读取操作花费的时间超过该值就会read_timeout
抛出错误。您可以Net::HTTP
从 Felipe Philipp 那里了解更多关于超时的信息。
我们可以采取一些措施来避免出现Net::ReadTimeout
错误。一旦您了解了引发错误的 HTTP 请求,就可以尝试将值调整read_timeout
为更合理的值。正如上文所述,如果您发出请求的服务器需要很长时间才能整理响应,然后才能一次性发送所有响应,那么您需要一个更长的read_timeout
值。如果服务器以分块形式返回响应,那么您需要一个更短的值read_timeout
。
您可以read_timeout
通过在所使用的相应 HTTP 客户端上设置以秒为单位的值来设置:
使用 Net::HTTP
http = Net::HTTP.new(host, port, read_timout: 10)
使用 open-uri
open(url, read_timeout: 10)
使用 HTTParty
HTTParty.get(url, read_timeout: 10)
您不能总是相信另一台服务器会在您预期的超时时间内响应。如果您可以在后台运行带有重试功能的 HTTP 请求(例如Sidekiq),则可以减轻来自其他服务器的错误。不过,您需要处理服务器永远无法及时响应的情况。
如果您需要在控制器操作中运行 HTTP 请求,那么您应该修复Net::ReadTimeout
错误,并为用户提供其他体验,并在错误监控解决方案中跟踪它。例如:
def show
@post = Post.find(params[:slug])
begin
@comments = HTTParty.get(COMMENTS_SERVER, read_timeout: 10)
rescue Net::ReadTimeout => e
@comments = []
@error_message = "Comments couldn't be retrieved, please try again later."
Rollbar.error(e);
end
end
5. ActiveRecord::RecordNotUnique: PG::UniqueViolation
此错误消息专门针对 PostgreSQL 数据库,但 MySQL 和 SQLite 的 ActiveRecord 适配器也会抛出类似的错误。这里的问题是,应用程序中的数据库表在一个或多个字段上具有唯一索引,并且已向数据库发送违反该索引的事务。这是一个很难完全解决的问题,但让我们先看看容易解决的问题。
假设您创建了一个User
模型,并在迁移过程中确保了用户的电子邮件地址是唯一的。迁移可能如下所示:
class CreateUsers < ActiveRecord::Migration[5.1]
def change
create_table :users do |t|
t.string :email
t.timestamps
end
add_index :users, :email, unique: true
end
end
为了避免大多数情况,ActiveRecord::RecordNotUnique
您也应该在模型中添加唯一性验证User
。
class User < ApplicationRecord
validates_uniqueness_of :email
end
如果没有此验证,所有电子邮件地址在调用时都会被发送到数据库User#save
,如果它们不唯一,则会引发错误。但是,验证并不能保证这种情况不会发生。有关完整解释,您应该阅读文档的并发性和完整性部分validates_uniqueness_of
。简而言之,Rails 唯一性检查容易根据多个请求的操作顺序出现竞争条件。由于竞争条件的存在,这也使得此错误难以在本地复现。
要处理这个错误,需要了解一些背景信息。如果错误是由竞争条件引起的,那可能是因为用户错误地提交了两次表单。我们可以尝试用一些 JavaScript 代码来缓解这个问题,在第一次点击后禁用提交按钮。可以尝试以下代码:
const forms = document.querySelectorAll('form');
Array.from(forms).forEach((form) => {
form.addEventListener('submit', (event) => {
const buttons = form.querySelectorAll('button, input[type=submit]')
Array.from(buttons).forEach((button) => {
button.setAttribute('disabled', 'disabled');
});
});
});
Coderwall 上的这个技巧,在错误发生时使用 ActiveRecordfirst_or_create!
并进行救援和重试,是一个巧妙的解决方法。您应该继续使用错误监控解决方案记录错误,以便保持对错误的可见性。
def self.set_flag( user_id, flag )
# Making sure we only retry 2 times
tries ||= 2
flag = UserResourceFlag.where( :user_id => user_id , :flag => flag).first_or_create!
rescue ActiveRecord::RecordNotUnique => e
Rollbar.error(e)
retry unless (tries -= 1).zero?
end
ActiveRecord::RecordNotUnique
这可能看起来像是一个极端情况,但它在前 10 名中排名第 5,因此绝对值得考虑您的用户体验。
6. NoMethodError:未定义方法‘id’为nil:NilClass
NoMethodError
再次出现,但这次的解释性消息有所不同。此错误通常在创建关联对象时潜伏。通常情况下,成功创建对象即可解决问题,但验证失败时会弹出此错误。我们来看一个例子。
这是一个具有为课程创建应用程序的操作的控制器。
class CourseApplicationsController < ApplicationController
def new
@course_application = CourseApplication.new
@course = Course.find(params[:course_id])
end
def create
@course_application = CourseApplication.new(course_application_params)
if @course_application.save
redirect_to @course_application, notice: 'Application submitted'
else
render :new
end
end
private
def course_application_params
params.require(:course_application).permit(:name, :email, :course_id)
end
end
新模板中的表单看起来有点像这样:
<%= form_for [@course, @course_application] do |ca| %>
<%# rest of the form %>
<% end %>
这里的问题是,当你render :new
从create
操作中调用时,@course
实例变量未设置。你需要确保模板所需的所有对象也在操作new
中初始化。为了修复此错误,我们将操作更新为如下所示:create
create
def create
@course_application = CourseApplication.new(course_application_params)
if @course_application.save
redirect_to @course_application, notice: 'Application submitted'
else
@course = Course.find(params[:course_id])
render :new
end
end
如果您有兴趣了解有关 Rails 中的问题nil
以及如何避免这些问题的更多信息,请查看本文。
7. ActionController::ParameterMissing
此错误是 Rails强参数ActionController::Base
实现的一部分。但它不会表现为 500 错误,而是被 400 Bad Request 错误修复并返回。
完整的错误可能如下所示:
ActionController::ParameterMissing: param is missing or the value is empty: user
这将伴随一个可能看起来有点像这样的控制器:
class UsersController < ApplicationController
def create
@user = User.new(user_params)
if @user.save
redirect_to @user
else
render :new
end
end
private
def user_params
params.require(:user).permit(:name, :email)
end
end
这params.require(:user)
意味着如果user_params
被调用时params
没有:user
键或者params[:user]
为空,ActionController::ParameterMissing
则会引发。
如果您正在构建一个通过 Web 前端使用的应用程序,并且已经构建了一个表单来正确地将user
参数提交到此操作,那么缺少user
参数可能意味着有人在干扰您的应用程序。在这种情况下,400 Bad Request 响应可能是最佳响应,因为您无需顾虑潜在的恶意用户。
如果您的应用程序正在提供 API,那么 400 Bad Request 也是对缺少参数的适当响应。
8. ActionView::Template::Error:未定义的局部变量或方法
这是我们ActionView
前 10 个错误中唯一的一个,这是一个好兆头。视图渲染模板所需的工作越少越好。工作越少,错误就越少。但我们仍然遇到这个错误,你期望的变量或方法实际上并不存在。
这种情况在部分代码中最为常见,可能是因为在页面上包含局部变量的方式有很多种。假设你有一个_post.html.erb
包含博客文章模板和@post
控制器中设置的实例变量的部分代码,那么你可以像这样渲染它:
<%= render @post %>
或者
<%= render 'post', post: @post %>
或者
<%= render partial: 'post', locals: { post: @post } %>
Rails 喜欢为我们提供大量的选项,但这里的第二和第三个选项很容易让人感到困惑。尝试渲染如下部分:
<%= render 'post', locals: { post: @post } %>
或者
<%= render partial: 'post', post: @post %>
会导致局部变量或方法未定义。为了避免这种情况,请保持一致,并始终使用显式部分语法渲染部分代码,并在 locals 哈希中表达局部变量:
<%= render partial: 'post', locals: { post: @post } %>
在部分代码中使用局部变量还有一个容易出错的地方。如果你只是偶尔将变量传递给部分代码,那么在部分代码中测试该变量的方式与在常规 Ruby 代码中有所不同。例如,如果你更新上面的帖子部分代码,使其接受一个局部变量来指示是否在部分代码中显示标题图片,那么你应该像这样渲染该部分代码:
<%= render partial: 'post', locals: { post: @post, show_header_image: true } %>
那么部分本身可能看起来像这样:
<h1><%= @post.title %></h1>
<%= image_tag(@post.header_image) if show_header_image %>
<!-- and so on -->
当你传递局部变量时,这将正常工作show_header_image
,但是当你调用
<%= render partial: 'post', locals: { post: @post } %>
它会因未定义的局部变量而失败。要测试局部变量是否存在,你应该在使用它之前检查它是否已定义。
<%= image_tag(@post.header_image) if defined?(show_header_image) && show_header_image %>
local_assigns
更好的是,我们可以在部分内容中调用一个哈希来代替它。
<%= image_tag(@post.header_image) if local_assigns[:show_header_image] %>
对于非布尔值的变量,我们可以使用其他哈希方法来fetch
优雅地处理。show_header_image
例如,以下场景也可以:
<%= image_tag(@post.header_image) if local_assigns.fetch(:show_header_image, false) %>
总的来说,当你将变量传递给部分变量时要小心!
9. ActionController::UnknownFormat
此错误与 类似,ActionController::InvalidAuthenticityToken
可能是由粗心或恶意的用户(而非您的应用程序)引起的。如果您构建了一个应用程序,其中的操作以 HTML 模板进行响应,而有人请求页面的 JSON 版本,您会在日志中发现此错误,如下所示:
ActionController::UnknownFormat (BlogPostsController#index is missing a template for this request format and variant.
request.formats: ["application/json"]
request.variant: []):
用户将收到 406 Not Acceptable 响应。在这种情况下,他们会看到此错误,因为您尚未为此响应定义模板。这是一个合理的响应,因为如果您不想返回 JSON,则表示他们的请求不可接受。
然而,你可能已经构建了 Rails 应用程序,使其在同一个控制器中响应常规 HTML 请求和更像 API 的 JSON 请求。一旦你开始这样做,你就定义了你想要响应的格式,任何不属于这些格式的请求也会导致错误ActionController::UnknownFormat
,返回 406 状态。假设你有一个博客文章索引,如下所示:
class BlogPostsController < ApplicationController
def index
respond_to do |format|
format.html { render :index }
end
end
end
对 JSON 发出请求将导致 406 响应,并且您的日志将显示这个不太明显的错误:
ActionController::UnknownFormat (ActionController::UnknownFormat):
这次的错误并不是因为缺少模板——这是一个故意的错误,因为你定义了唯一需要响应的格式是 HTML。但如果这不是故意的呢?
遗漏响应中想要支持的格式是很常见的。假设您在创建博客文章时需要响应 HTML 和 JSON 请求,以便您的页面能够支持 Ajax 请求。它可能如下所示:
class BlogPostsController < ApplicationController
def create
@blog_post = BlogPost.new(blog_post_params)
respond_to do |format|
if @blog_post.save
format.html { redirect blog_post_path(@blog_post) }
format.json { render json: @blog_post.to_json }
else
render :new
end
end
end
end
如果博客文章验证失败且未保存,则会引发此错误。在respond_to
代码块中,您需要render
在格式块的范围内调用此方法。为了处理失败,请重写此代码,如下所示:
class BlogPostsController < ApplicationController
def create
@blog_post = BlogPost.new(blog_post_params)
respond_to do |format|
if @blog_post.save
format.html { redirect blog_post_path(@blog_post) }
format.json { render json: @blog_post.to_json }
else
format.html { render :new }
format.json { render json: @blog_post.errors.to_json }
end
end
end
end
现在所有格式都已涵盖,不会再出现任何无意的ActionController::UnknownFormat
异常。
10. StandardError:发生错误,此迁移和所有后续迁移均被取消
前十名中的最后一项让我有点失望。StandardError
它是所有其他错误都应该继承的基错误类,所以在这里使用它会让错误感觉很普通,而实际上它是数据库迁移期间发生的错误。我更希望将此错误视为 的后代ActiveRecord::MigrationError
。不过我跑题了……
有很多因素可能导致迁移失败。例如,您的迁移可能与实际生产数据库不同步。在这种情况下,您需要深入研究,找出问题所在并进行修复。
不过,这里有一件事需要讨论:数据迁移。
如果您需要为表中的所有对象添加或计算一些数据,您可能会认为数据迁移是一个好主意。例如,如果您想在包含姓氏和名字的用户模型中添加一个全名字段(不太可能发生更改,但对于一个简单的示例来说已经足够了),您可以编写如下迁移:
class AddFullNameToUser < ActiveRecord::Migration
def up
add_column :users, :full_name, :string
User.find_each do |user|
user.full_name = "#{user.first_name} #{user.last_name}"
user.save!
end
end
def down
remove_column :users, :full_name
end
end
这种场景存在很多问题。如果 yx xour 集合中某个用户的数据损坏,该user.save!
命令将抛出错误并取消迁移。其次,在生产环境中,您可能拥有大量用户,这意味着数据库迁移需要很长时间,甚至可能导致应用程序一直处于离线状态。最后,随着应用程序的不断变化,您可能会删除或重命名User
模型,这会导致迁移失败。一些建议建议您User
在迁移中定义一个模型来避免这种情况。为了更加安全,Elle Meredith 建议我们完全避免在 ActiveRecord 迁移中进行数据迁移,而是构建临时数据迁移任务。
在迁移之外更改数据可以确保您完成一些操作。最重要的是,它让您考虑如果数据不存在,模型将如何工作。在我们的全名示例中,您可能会为full_name
属性定义一个访问器,以便在数据可用时做出响应。如果数据不可用,则通过连接各个组成部分来构建全名。
class User < ApplicationRecord
def full_name
@full_name || "#{first_name} #{last_name}"
end
end
将数据迁移作为单独的任务运行也意味着部署不再依赖于生产数据库中的数据更改。Elle的文章进一步解释了为什么这样做效果更好,并提供了编写任务的最佳实践。
结论
最常见的 Rails 错误可能来自应用程序的任何位置。在本文中,我们了解了模型、视图和控制器中常见的错误。有些错误无需担心,只是为了保护您的应用程序。而其他错误则应尽快发现并消除。
尽管如此,跟踪这些错误发生的频率还是有好处的。这能让你更好地了解影响用户或应用程序安全的问题,从而快速修复它们。否则,这些错误消息会显示给用户,但工程和产品管理团队在用户向支持部门投诉之前根本不知道。
我们希望您学到了新的知识,并更好地避免将来出现这些错误。然而,即使采用了最佳实践,生产环境中也难免会出现意外错误。了解影响用户的错误,并拥有快速解决问题的良好工具至关重要。
Rollbar 让您能够查看生产环境中的 Ruby 错误,从而提供更多上下文信息以便快速解决问题,包括表单验证错误、人员追踪、启动错误等等。查看Rollbar 的完整功能列表和Ruby SDK 文档。
《1000 多个 Ruby on Rails 项目中的十大错误(以及如何避免它们)》最初于 2018 年 4 月 18 日发布在Rollbar 博客上。
文章来源:https://dev.to/philnash/top-10-errors-from-1000-ruby-on-rails-projects-and-how-to-avoid-them-24m