如何在 Rails 中重定向到 404?

Posted

技术标签:

【中文标题】如何在 Rails 中重定向到 404?【英文标题】:How to redirect to a 404 in Rails? 【发布时间】:2011-01-24 01:19:43 【问题描述】:

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

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

Rails 是如何做到的?

【问题讨论】:

【参考方案1】:

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

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

Rails 也以同样的方式处理 AbstractController::ActionNotFoundActiveRecord::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 render 404 not found from a controller action的更多信息

【讨论】:

有理由自己动手。如果您的应用程序从根目录劫持了所有路由。这是一个糟糕的设计,但有时是不可避免的。 这种方法还允许您使用 ActiveRecord bang finder(find!、find_by_...! 等),如果没有找到记录,它们都会引发 ActiveRecord::RecordNotFound 异常(触发 rescue_from处理程序)。 这会引发 500 内部服务器错误,而不是 404。我错过了什么? 似乎ActionController::RecordNotFound 是更好的选择? 代码运行良好,但测试没有,直到我意识到我使用的是具有不同语法的 RSpec 2:expect visit '/something/you/want/to/404' .to raise_error(ActionController::RoutingError) /via ***.com/a/1722839/993890【参考方案2】:

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

这意味着你不需要自己拯救这个动作

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

【讨论】:

这个方案有一个大问题;它仍然会运行模板中的代码。因此,如果您有一个简单、宁静的结构并且有人输入了一个不存在的 ID,那么您的模板将寻找不存在的对象。 如前所述,这不是正确答案。试试史蒂文的。 更改了所选答案以反映更好的做法。感谢 cmets,伙计们! 我用更多示例和有关 ActiveRecord 的注释更新了答案。 bang 版本确实会停止代码执行,因此恕我直言,这是更有效的解决方案。【参考方案3】:

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

【讨论】:

真的很喜欢这个答案,尤其是关于输出的测试,而不是控制器中调用的方法…… Rails 内置 404 状态:render :text => 'Not Found', :status => :not_found @JaimeBellmyer - 我敢肯定,当您处于已部署(即登台/生产)环境中时,它确实不会返回 200。我在几个应用程序中执行此操作,并且它按照接受的解决方案中的描述工作。也许您所指的是它在呈现开发中的调试屏幕时返回 200,您可能在 environments/development.rb 文件中将 config.consider_all_requests_local 参数设置为 true。如果您提出错误,如已接受的解决方案中所述,在暂存/生产中,您肯定会得到 404,而不是 200。【参考方案4】:

你也可以使用渲染文件:

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

您可以在哪里选择使用或不使用布局。

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

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

【讨论】:

【参考方案5】:

所选答案在 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 有不同的看法。

【讨论】:

这是一个非常好的解决方案;但是,您不需要|| not_found 部分,只需调用find!(注意爆炸声),当无法检索资源时它会抛出 ActiveRecord::RecordNotFound。另外,在 if 条件下将 ActiveRecord::RecordNotFound 添​​加到数组中。 我会拯救 StandardError 而不是 Exception,以防万一。实际上,我将保留标准的 500 静态页面,根本不使用自定义 render_500,这意味着我将明确 rescue_from 与 404 相关的错误数组【参考方案6】:

这些会帮助你...

应用控制器

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-lghref: root_path
          %span.glyphicon.glyphicon-home
          Take Me Home

【讨论】:

【参考方案7】:
<%= render file: 'public/404', status: 404, formats: [:html] %>

只需将此添加到要呈现到 404 错误页面的页面即可。

【讨论】:

【参考方案8】:
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

【讨论】:

【参考方案9】:

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

【讨论】:

【参考方案10】:

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

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

【讨论】:

【参考方案11】:

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

我这里把基本逻辑放在ApplicationController中,但也可以放在更具体的控制器中,只为一个控制器设置特殊的逻辑。

我使用带有 ENV['RESCUE_404'] 的 if 的原因是我可以单独测试 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

【讨论】:

【参考方案12】:

提升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

这种方法的好处在于:

    它像普通的 ActionController::RoutingError 一样挂钩到现有的错误处理中间件,但在开发环境中您会收到更有意义的错误消息 它会正确地将状态设置为您在救援响应哈希中指定的任何内容(在本例中为 404 - not_found) 您不必编写需要随处可用的not_found 方法。

【讨论】:

以上是关于如何在 Rails 中重定向到 404?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Microsoft Azure 存档 Blob 中重定向状态 404

如何在typo3 V10的extbase控制器中重定向到404页面

如何在NGINX中重定向一个网址

如何在控制器操作中重定向到 aspx 页面

如何在PHP中重定向到同一页面

如何在 Spring 中重定向到另一台主机