如何在Rails中重定向到404?


Answers:


1049

不要自己渲染404,没有理由;Rails已经内置了此功能。如果要显示404页面,请按以下render_404方式创建一个方法(或not_found如我所说的ApplicationController那样):

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

也导轨手柄AbstractController::ActionNotFound,并ActiveRecord::RecordNotFound以同样的方式。

这样可以更好地完成两件事:

1)它使用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的更多信息


3
有理由自己做。如果您的应用程序从根目录劫持了所有路由。这是不好的设计,但有时是不可避免的。
ablemike

7
这种方法还允许您使用ActiveRecord爆炸查找器(find!,find_by _...!等),如果未找到任何记录,则它们都会引发ActiveRecord :: RecordNotFound异常(触发result_from处理程序)。
gjvis 2012年

2
这为我引发了500 Internal Server Error,而不是404。我缺少什么?
Glenn

3
似乎ActionController::RecordNotFound是更好的选择?
彼得·埃里希

4
代码运行良好,但是直到我意识到我正在使用具有不同语法的RSpec 2才进行了测试:expect { visit '/something/you/want/to/404' }.to raise_error(ActionController::RoutingError)/ via stackoverflow.com/a/1722839/993890
ryanttb

243

HTTP 404状态

要返回404标头,只需:status对render方法使用该选项。

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错误,例如ActiveRecord::RecordNotFound显示404错误页面。

这意味着您无需自己挽救该动作

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

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

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

您可以通过将检查委托给Rails来简化。只需使用爆炸版本。

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

9
这个解决方案有一个大问题;它将仍然运行模板中的代码。因此,如果您有一个简单,宁静的结构,并且有人输入了一个不存在的ID,那么您的模板将查找不存在的对象。
jcalvert 2011年

5
如前所述,这不是正确的答案。试试史蒂文的。
Pablo Marambio

更改了所选答案以反映更好的做法。谢谢大家的评论!
Yuval Karmi

1
我用更多示例和有关ActiveRecord的注释更新了答案。
西蒙妮·卡列蒂

1
爆炸式版本确实会停止执行代码,因此它是更有效的解决方案恕我直言。
Gui vieira

60

史蒂文·索罗卡(Steven Soroka)提交的最新选定答案很接近,但还不完整。测试本身掩盖了一个事实,即它没有返回真正的404-它返回的状态为200-“成功”。最初的答案更接近,但试图呈现布局,好像没有发生任何故障。这可以解决所有问题:

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”。您关心的是输出。我上面的测试允许我尝试各种解决方案,无论解决方案是否引发异常,特殊渲染等,测试均保持不变。


3
非常喜欢这个答案,特别是关于输出的测试,而不是控制器中调用的方法……
xentek 2013年

Rails具有内置404状态:render :text => 'Not Found', :status => :not_found
Lasse Bunk

1
@JaimeBellmyer -我敢肯定它并没有返回200,当你在部署(即分期/生产)环境是。我在几个应用程序中都执行此操作,并且它按照公认的解决方案中的描述进行工作。也许您指的是在开发中渲染调试屏幕时返回的200,其中文件中的config.consider_all_requests_local参数可能设置为true environments/development.rb。如果你抛出一个错误,因为在接受的解决方案所描述的,在分期/生产,你一定会得到一个404,而不是200
Javid詹美回教堂

18

您还可以使用渲染文件:

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

您可以选择是否使用布局的位置。

另一种选择是使用“异常”来控制它:

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

13

由于错误处理程序已移至中间件,因此所选答案在Rails 3.1+中不起作用(请参阅github issue)。

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

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

当然可以改善这一点,但至少在不覆盖Rails核心功能的情况下,我对not_found和internal_error有不同的看法。


3
这是一个非常好的解决方案;但是,您不需要该|| not_found部分,只需调用find!(请注意爆炸),当无法检索资源时,它将抛出ActiveRecord :: RecordNotFound。另外,在if条件下,将ActiveRecord :: RecordNotFound添加到数组中。
MarekPříhoda2014年

1
为了以防万一,我会救命StandardError而不是Exception。实际上,我将保留标准500静态页面,并且完全不使用自定义render_500,这意味着我将明确显示rescue_from与404相关的错误数组
Dr.Strangelove

7

这些将帮助您...

应用控制器

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

views / errors / 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


1

我想为不是管理员的所有登录用户抛出“正常” 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

1
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

0

要测试错误处理,您可以执行以下操作:

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

0

如果要以不同方式处理不同的404,请考虑在控制器中捕获它们。这将使您能够执行以下操作,例如跟踪由不同用户组生成的404数量,与用户进行交互以找出问题所在/可能需要调整用户体验的哪一部分,进行A / B测试等。

我在这里将基本逻辑放置在ApplicationController中,但也可以将其放置在更特定的控制器中,以仅对一个控制器具有特殊的逻辑。

我将if与ENV ['RESCUE_404']一起使用的原因是,我可以单独测试AR :: RecordNotFound的提高。在测试中,我可以将此ENV var设置为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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.