Как перенаправить на 404 в Rails?


482

Я хотел бы «подделать» страницу 404 в Rails. В PHP я бы просто отправил заголовок с кодом ошибки как таковой:

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

Как это сделать с Rails?

Ответы:


1049

Не визуализируйте 404 самостоятельно, нет причин для этого; В Rails эта функциональность уже встроена. Если вы хотите показать страницу 404, создайте render_404метод (или not_foundкак я его назвал) ApplicationControllerследующим образом:

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

Rails тоже обрабатывает AbstractController::ActionNotFound, и так ActiveRecord::RecordNotFoundже.

Это делает две вещи лучше:

1) Он использует встроенный rescue_fromобработчик Rails для рендеринга страницы 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
Есть причина сделать это самостоятельно. Если ваше приложение перехватывает все маршруты из корня. Это плохой дизайн, но иногда его невозможно избежать.
возможность

7
Этот подход также позволяет использовать средства поиска взрыва ActiveRecord (find !, find_by _...! И т. Д.), Которые вызывают исключение ActiveRecord :: RecordNotFound, если запись не найдена (вызывая обработчик rescue_from).
gjvis

2
Это вызывает 500 внутренних ошибок сервера для меня, а не 404. Что мне не хватает?
Гленн

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опцию для метода рендеринга.

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 проверку. Просто используйте версию взрыва.

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

9
Есть большая проблема с этим решением; он все равно будет запускать код в шаблоне. Поэтому, если у вас простая, спокойная структура и кто-то вводит несуществующий идентификатор, ваш шаблон будет искать объект, который не существует.
jcalvert

5
Как упоминалось ранее, это не правильный ответ. Попробуйте Стивена.
Пабло Марамбио

Изменен выбранный ответ, чтобы отразить лучшую практику. Спасибо за комментарии, ребята!
Юваль Карми

1
Я обновил ответ большим количеством примеров и заметкой об ActiveRecord.
Симона Карлетти

1
Взрывная версия останавливает выполнение кода, так что это более эффективное решение, IMHO.
Gui Vieira

60

Недавно выбранный ответ, представленный Стивеном Сорокой, близок, но не завершен. Сам тест скрывает тот факт, что это не возвращает истинный 404 - он возвращает статус 200 - «успех». Первоначальный ответ был ближе, но попытался отобразить макет, как если бы не произошло никакого сбоя. Это исправляет все:

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

Вот типичный мой тестовый набор для чего-то, что я ожидаю вернуть 404, используя сопоставители RSpec и Shoulda:

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

Эта здоровая паранойя позволила мне обнаружить несоответствие типов контента, когда все остальное выглядело замечательно :) Я проверяю все эти элементы: назначенные переменные, код ответа, тип контента ответа, представленный шаблон, отображаемый макет, флэш-сообщения.

Я пропущу проверку типа контента в приложениях, которые строго HTML ... иногда. Ведь «скептик проверяет ВСЕ ящики» :)

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

К вашему сведению: я не рекомендую проверять то, что происходит в контроллере, то есть «should_raise». Что вас волнует, так это результат. Мои тесты, приведенные выше, позволили мне попробовать различные решения, и тесты остаются прежними, независимо от того, вызывает ли решение исключение, специальный рендеринг и т. Д.


3
Мне очень нравится этот ответ, особенно в отношении тестирования выходных данных, а не методов, вызываемых в контроллере…
xentek

Rails имеет встроенный 404 статус: render :text => 'Not Found', :status => :not_found.
Лассе Банк

1
@JaimeBellmyer - я уверен, что он не возвращает 200, когда вы находитесь в развернутой (т.е. промежуточной / рабочей) среде. Я делаю это в нескольких приложениях, и это работает, как описано в принятом решении. Возможно, вы имеете в виду, что он возвращает 200, когда отображает экран отладки в разработке, где у вас, вероятно, установлен config.consider_all_requests_localпараметр true в вашем environments/development.rbфайле. Если вы поднимете ошибку, как описано в принятом решении, в постановке / производстве, вы обязательно получите 404, а не 200.
Джавид Джамае

18

Вы также можете использовать файл рендеринга:

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

Где вы можете использовать макет или нет.

Другой вариант - использовать исключения для управления им:

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

13

Выбранный ответ не работает в 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.


3
это очень хорошее решение; тем не менее, вам не нужна || not_foundчасть, просто вызовите find!(заметьте взрыв), и он выдаст ActiveRecord :: RecordNotFound, когда ресурс не может быть получен. Также добавьте ActiveRecord :: RecordNotFound в массив в условии if.
Марек Пршихода

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

просмотров / ошибки / 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

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

просто добавьте это на страницу, которую вы хотите отобразить на странице ошибки 404, и все готово.


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
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.