1000多个Ruby项目中的前10个错误(以及如何避免这些错误)

分享于 

26分钟阅读

Ruby

  繁體

你可能已经注意到,

我们提供基于Rails 5的示例解决方案,但是如果你仍在使用Rails 4,它们应该为你指明正确的方向。

1. ActionController::RoutingError

我们从典型的Web应用程序开始,即404错误的Rails版本,一个 ActionController::RoutingError 表示用户已请求了应用程序中不存在的URL ,Rails会记录该错误,它看起来像是一个错误,但是在大多数情况下,这并不是应用程序的错误。

可能是由于不正确的链接指向或来自你的应用程序,也可能是恶意用户或bot测试应用程序是否存在常见弱点,如果是这样,在日志中发现类似这样的情况:

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 那么通过设置一条全部捕获的route并自己提供404服务来避免它们,在 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

2. NoMethodError : undefined method'[]'for nil:NilClass

这意味着使用方括号表示法从对象中读取属性,但是缺少对象或nil,因此不支持此方法,很可能正在通过散列或数组来访问属性,并且路径上缺少一些东西,当你从JSON API或CSV文件中解析和提取数据,或者只是在控制器操作中从嵌套参数获取数据时,最容易发生这种情况。

你可能希望参数如下所示:

{ user:{ address:{ street:'123 Fake Street', town:'Faketon', postcode:'12345'}}}

然后你可以通过调用 params[:user][:address] 结果发生nil 调用[:street] 会引发 NoMethodError

你可以对每个参数执行nil检查,并在使用&& 运算符,如:

street = params[:user]&& params[:user][:address]&& params[:user][:address][:street]

虽然这可以工作,但现在有一种更好的方法来访问哈希,数组和事件对象,比如ActionController::Parametersdig 允许提供要检索的对象的路径。 nildignil

street = params.dig(:user,:address,:street)

streetnil

所以,而不是调用

street = user.address.street

得到一个 NoMethodError: undefined method street

street = user&.address&.street

nilstreetnil 你需要处理 nil 以后引用 street 将正确分配。

尽管这样可以防止向用户显示错误,但是如果它仍然影响用户体验,你可能希望创建一个内部错误以便在日志或Rollbar之类的错误跟踪系统中进行跟踪,以便可以解决问题。

3. ActionController::InvalidAuthenticityToken


与我们应用程序的安全性有关,因此需要仔细考虑是是ActionController::InvalidAuthenticityToken 或删除请求丢失或CSRF (跨站点请求伪造)令牌不正确。

CSRF是web应用程序中的潜在漏洞,恶意站点伪装未知用户向应用程序发出请求,如果用户已登录,则他们的会话cookie将与请求一起发送,攻击者可以伪装用户身份执行命令。

Rails缓解CSRF攻击 通过在站点已知和验证的所有表单中包含安全令牌,但第三方不能知道。 ApplicationController


class ApplicationController < ActionController::Base
 protect_from_forgery with: :exception
end

ActionController::InvalidAuthenticityToken 错误可能意味着攻击者正在以你网站的用户为攻击目标,但是Rails的安全措施可以保护你的网站。

Ajax

例如,如果从前端发出Ajax请求,则需要确保在请求中包含CSRF令牌,如果你使用jQuery,, 这已经为你内置处理好了,如果你想以其他方式处理Ajax,请使用Fetch API,你需要确保包含CSRF令牌,对于这两种方法,都需要确保CSRF meta tag在<head>

<%= csrf_meta_tags %>

打印出如下所示的meta标记:

<meta name='csrf-token' content='THE-TOKEN'>

在发出Ajax请求时,读取meta标记内容并将它作为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
});

Webhooks/APIs

有时会需要关闭CSRF保护,比如,如果希望从第三方接收对应用程序中的某些URL的POST请求,则不希望根据CSRF接收这些请求,如果你正在为第三方开发人员构建API,或者希望接收来自服务的传入Webhooks,则可能需要关闭。

你可以关闭CSRF保护,但是要确保使用白名单的端点,通过跳过身份验证在控制器中执行此操作:


class ApiController < ApplicationController
 skip_before_action :verify_authenticity_token
end

如果你接受传入的Webhooks,则应该能够验证请求来自受信任的来源,而不是验证CSRF令牌。

4. Net::ReadTimeout

Net::ReadTimeout 当Ruby从套接字读取数据所需的时间长于 read_timeout 值,默认为60秒,如果正在使用 Net::HTTPopen-uri 或者 发出HTTP请求。

read_timeout

我们可以停止获取 Net::ReadTimeout错误,一旦知道了引发错误的HTTP请求,就可以尝试调整read_timeoutread_timeout 值.如果服务器以块的形式返回响应,则可以使用更短的read_timeout

你可以设置 read_timeout

使用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,它可以减少来自其他服务器的错误,但是,你需要处理服务器不及时响应的情况。

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 如果不唯一,将引发错误,但是,验证不能保证不会发生这种情况, 基于多个请求的操作顺序,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');
 });
 });
});

当错误被引发时,一个整洁的解决方法


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 : undefined method'id'for 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 :newcreate 动作,@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

7. ActionController::ParameterMissing

此错误是Rails的一部分 强参数 实施,它不会显示为500错误,而是由 ActionController::Base 并作为400错误请求返回。

完整的错误可能如下所示:

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 将将出现。

UserUser 如果是这样,400错误请求响应可能是最好的响应,因为你不需要迎合潜在的恶意用户。

如果你的应用程序正在提供API,则400 Bad Request也是对缺少参数的适当响应。

8. ActionView::Template::Error : undefined local variable or method

ActionView 前十名中的错误,这是一个好兆头,视图渲染模板所需的工作越少越好。

这可能是因为在局部变量中包含页面的许多不同方法导致了最常见的局部变量,_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%>

会留下一个未定义的局部变量或方法,为了避免这种情况,请保持一致并始终使用显式的部分语法渲染部分,在局部变量hash中表示局部变量:

<%= render partial:'post', locals:{ post:@post}%>

例如,如果你更新上面的postpartial以使用一个局部变量,该局部变量告诉你是否在partial中显示标题图像,则可以像这样渲染partial :

<%= 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)ifdefined?(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不可接受的响应,在这种情况下,他们将看到此错误,因为你没有为此响应定义模板,这是一个合理的响应,因为如果你不想返回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和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 : an error has occurred , this and all later migrations canceled

是所有其他错误都应该继承的基类,因此在这里使用它会使错误感觉非常通用,在现实中,它是在数据库迁移期间发生的错误,我宁愿将此错误视为 ActiveRecord::MigrationError

有很多事情会导致迁移失败,例如,你的迁移可能与实际的生产数据库不同步,

这里应该有一件事情要介绍: 数据迁移。

如果需要为表中的所有对象添加或计算某些数据,你认为数据迁移是一个好主意,例如,如果你想向包含它名字和姓氏的用户模型添加一个 full name字段(可能的更改,但是对于一个简单的示例来说已经足够了),则可以这样编写迁移:


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

这个场景有许多问题,如果yxxour set中的用户数据损坏,则 user.save! 命令将引发错误并取消迁移,其次,在生产中,你可能有许多用户,这意味着数据库可能需要很长时间才能离线迁移,最后,随着应用程序的更改,你可以删除或重命名User模型,这将导致迁移失败,User 为了获得更大的安全性,Elle Meredith建议我们避免在ActiveRecord迁移中进行数据迁移 完全和构建临时数据迁移任务。

在迁移之外更改数据可以确保执行一些操作,在我们的full name示例中,你可能需要为 full_name 当数据可用时可以响应的属性,如果不是,则通过连接来构建全名。


class User < ApplicationRecord
 def full_name
 @full_name || "#{first_name} #{last_name}"
 end
end

proj  rails  Projects  errors  
相关文章