如何重定向到一个404在Rails?

我想在Rails中“伪造”一个404页面。在PHP中,我只会发送一个带有错误代码的报头:

header("HTTP/1.0 404 Not Found");

Rails如何做到这一点?

265700 次浏览

HTTP 404状态

要返回404标头,只需使用渲染方法的:status选项。

def action
# here the code


render :status => 404
end

如果希望呈现标准404页面,可以在方法中提取该特性。

def render_404
respond_to do |format|
format.html { render :file => "#{Rails.root}/public/404", :layout => false, :status => :not_found }
format.xml  { head :not_found }
format.any  { head :not_found }
end
end

把它称为你的行动

def action
# here the code


render_404
end

如果希望操作呈现错误页面并停止,只需使用return语句。

def action
render_404 and return if params[:something].blank?


# here the code that will never be executed
end

ActiveRecord和HTTP 404

还要记住,Rails会挽救一些ActiveRecord错误,例如显示404错误页面的ActiveRecord::RecordNotFound

这意味着您不需要自己拯救这个操作

def show
user = User.find(params[:id])
end

当用户不存在时,User.find将引发ActiveRecord::RecordNotFound。这是一个非常强大的功能。请看下面的代码

def show
user = User.find_by_email(params[:email]) or raise("not found")
# ...
end

您可以通过将检查委托给Rails来简化它。简单地使用bang版本。

def show
user = User.find_by_email!(params[:email])
# ...
end

不要自己渲染404页面,没有理由;Rails已经内置了这个功能。如果你想显示一个404页面,在ApplicationController中创建一个render_404方法(或我称之为not_found),如下所示:

def not_found
raise ActionController::RoutingError.new('Not Found')
end

Rails也以同样的方式处理AbstractController::ActionNotFoundActiveRecord::RecordNotFound

这样做有两个好处:

它使用Rails内置的rescue_from处理程序来呈现404页面,并且 2)它中断你的代码的执行,让你做一些不错的事情,如:

  user = User.find_by_email(params[:email]) or not_found
user.do_something!

而不用写丑陋的条件语句。

作为奖励,它在测试中也非常容易处理。例如,在rspec集成测试中:

# RSpec 1


lambda {
visit '/something/you/want/to/404'
}.should raise_error(ActionController::RoutingError)


# RSpec 2+


expect {
get '/something/you/want/to/404'
}.to raise_error(ActionController::RoutingError)

和小型试验:

assert_raises(ActionController::RoutingError) do
get '/something/you/want/to/404'
end

或从Rails在控制器操作中未发现404引用更多信息

由Steven Soroka提交的新选答案很接近,但不完整。测试本身隐藏了这样一个事实:这并不是返回一个真正的404,而是返回一个状态为200的“success”。原来的答案更接近,但试图呈现的布局,好像没有发生失败。这可以解决所有问题:

render :text => 'Not Found', :status => '404'

下面是我的一个典型的测试集,使用RSpec和Shoulda匹配器,我希望返回404:

describe "user view" do
before do
get :show, :id => 'nonsense'
end


it { should_not assign_to :user }


it { should respond_with :not_found }
it { should respond_with_content_type :html }


it { should_not render_template :show }
it { should_not render_with_layout }


it { should_not set_the_flash }
end

我检查了所有这些元素:赋值变量、响应代码、响应内容类型、模板渲染、布局渲染、flash消息。

我将跳过严格的html应用程序的内容类型检查……有时。毕竟,“怀疑论者会检查所有的抽屉”:)

http://dilbert.com/strips/comic/1998-01-20/

仅供参考:我不建议测试控制器中发生的事情,即“should_raise”。你关心的是输出。上面的测试允许我尝试各种解决方案,无论解决方案是否引发异常、特殊呈现等,测试都保持不变。

所选的答案在Rails 3.1+中不起作用,因为错误处理程序被移动到中间件(参见github的问题)。

这是我找到的解决方案,我很满意。

ApplicationController:

  unless Rails.application.config.consider_all_requests_local
rescue_from Exception, with: :handle_exception
end


def not_found
raise ActionController::RoutingError.new('Not Found')
end


def handle_exception(exception=nil)
if exception
logger = Logger.new(STDOUT)
logger.debug "Exception Message: #{exception.message} \n"
logger.debug "Exception Class: #{exception.class} \n"
logger.debug "Exception Backtrace: \n"
logger.debug exception.backtrace.join("\n")
if [ActionController::RoutingError, ActionController::UnknownController, ActionController::UnknownAction].include?(exception.class)
return render_404
else
return render_500
end
end
end


def render_404
respond_to do |format|
format.html { render template: 'errors/not_found', layout: 'layouts/application', status: 404 }
format.all { render nothing: true, status: 404 }
end
end


def render_500
respond_to do |format|
format.html { render template: 'errors/internal_server_error', layout: 'layouts/application', status: 500 }
format.all { render nothing: true, status: 500}
end
end

application.rb中:

config.after_initialize do |app|
app.routes.append{ match '*a', :to => 'application#not_found' } unless config.consider_all_requests_local
end

在我的资源(显示,编辑,更新,删除):

@resource = Resource.find(params[:id]) or not_found

这当然可以改进,但至少,我对not_found和internal_error有不同的视图,而不重写核心Rails函数。

这些会帮助你……

应用程序控制器

class ApplicationController < ActionController::Base
protect_from_forgery
unless Rails.application.config.consider_all_requests_local
rescue_from ActionController::RoutingError, ActionController::UnknownController, ::AbstractController::ActionNotFound, ActiveRecord::RecordNotFound, with: lambda { |exception| render_error 404, exception }
end


private
def render_error(status, exception)
Rails.logger.error status.to_s + " " + exception.message.to_s
Rails.logger.error exception.backtrace.join("\n")
respond_to do |format|
format.html { render template: "errors/error_#{status}",status: status }
format.all { render nothing: true, status: status }
end
end
end

错误的控制器

class ErrorsController < ApplicationController
def error_404
@not_found_path = params[:not_found]
end
end

视图/错误/ error_404.html.haml

.site
.services-page
.error-template
%h1
Oops!
%h2
404 Not Found
.error-details
Sorry, an error has occured, Requested page not found!
You tried to access '#{@not_found_path}', which is not a valid page.
.error-actions
%a.button_simple_orange.btn.btn-primary.btn-lg{href: root_path}
%span.glyphicon.glyphicon-home
Take Me Home

为了测试错误处理,你可以这样做:

feature ErrorHandling do
before do
Rails.application.config.consider_all_requests_local = false
Rails.application.config.action_dispatch.show_exceptions = true
end


scenario 'renders not_found template' do
visit '/blah'
expect(page).to have_content "The page you were looking for doesn't exist."
end
end

你也可以使用渲染文件:

render file: "#{Rails.root}/public/404.html", layout: false, status: 404

你可以选择是否使用该布局。

另一种选择是使用Exceptions来控制它:

raise ActiveRecord::RecordNotFound, "Record not found."

如果希望以不同的方式处理不同的404,可以考虑在控制器中捕获它们。这将允许你做一些事情,如跟踪不同用户组生成的404数量,让支持人员与用户交互,找出哪里出了问题/用户体验的哪一部分可能需要调整,进行A/B测试等。

我在这里将基本逻辑放在ApplicationController中,但它也可以放在更特定的控制器中,以便只针对一个控制器拥有特殊的逻辑。

我使用ENV['RESCUE_404']的if的原因是,这样我就可以单独测试AR::RecordNotFound的提升。在测试中,我可以将这个ENV变量设置为false,并且我的rescue_from不会触发。这样我就可以从条件404逻辑中分离出来测试引发。

class ApplicationController < ActionController::Base


rescue_from ActiveRecord::RecordNotFound, with: :conditional_404_redirect if ENV['RESCUE_404']


private


def conditional_404_redirect
track_404(@current_user)
if @current_user.present?
redirect_to_user_home
else
redirect_to_front
end
end


end
<%= render file: 'public/404', status: 404, formats: [:html] %>

只需将此添加到要呈现到404错误页面的页面,就完成了。

我想对任何不是管理员的登录用户抛出一个“正常的”404,所以我最终在Rails 5中写了这样的东西:

class AdminController < ApplicationController
before_action :blackhole_admin


private


def blackhole_admin
return if current_user.admin?


raise ActionController::RoutingError, 'Not Found'
rescue ActionController::RoutingError
render file: "#{Rails.root}/public/404", layout: false, status: :not_found
end
end
routes.rb
get '*unmatched_route', to: 'main#not_found'


main_controller.rb
def not_found
render :file => "#{Rails.root}/public/404.html", :status => 404, :layout => false
end

抛出ActionController::RoutingError('not found')对我来说总是感觉有点奇怪——在未经过身份验证的用户的情况下,这个错误并不反映现实——路由被找到了,用户只是没有经过身份验证。

我偶然遇到config.action_dispatch.rescue_responses,我认为在某些情况下,这是一个更优雅的解决方案的问题:

# application.rb
config.action_dispatch.rescue_responses = {
'UnauthenticatedError' => :not_found
}


# my_controller.rb
before_action :verify_user_authentication


def verify_user_authentication
raise UnauthenticatedError if !user_authenticated?
end

这种方法的好处在于:

  1. 它像普通的ActionController::RoutingError一样钩子到现有的错误处理中间件中,但是在开发环境中你会得到一个更有意义的错误消息
  2. 它将正确地将状态设置为您在rescue_responses散列中指定的任何状态(在本例中为404 - not_found)
  3. 你不必编写一个not_found方法,它需要在任何地方都可用。