Jak przekierować na 404 w Railsach?


Odpowiedzi:


1049

Nie renderuj 404 samemu, nie ma powodu; Railsy mają już tę funkcję wbudowaną. Jeśli chcesz wyświetlić stronę 404, utwórz render_404metodę (lub not_foundjak ją nazwałem) w ApplicationControllernastępujący sposób:

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

Poręcze również obsługują AbstractController::ActionNotFound iActiveRecord::RecordNotFound ten sam sposób.

To robi dwie rzeczy lepiej:

1) Używa wbudowanego rescue_frommodułu obsługi Railsów do renderowania strony 404, i 2) przerywa wykonywanie kodu, pozwalając ci robić fajne rzeczy, takie jak:

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

bez konieczności pisania brzydkich instrukcji warunkowych.

Jako bonus, jest również bardzo łatwy w obsłudze w testach. Na przykład w teście integracji 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)

I najmniejszy:

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

LUB odnieś więcej informacji z Railsów, render 404 nie znaleziono z działania kontrolera


3
Jest powód, aby zrobić to sam. Jeśli aplikacja przejmie wszystkie trasy z katalogu głównego. To zły projekt, ale czasem nie da się go uniknąć.
— ablemike

7
Podejście to pozwala także używać wyszukiwarek hukowych ActiveRecord (find !, find_by _...! Itd.), Które wszystkie zgłaszają wyjątek ActiveRecord :: RecordNotFound, jeśli nie zostanie znaleziony żaden rekord (wyzwalanie procedury ratowania_od obsługi).
— gjvis

2
To powoduje 500 wewnętrzny błąd serwera, a nie 404. Czego mi brakuje?
— Glenn

3
Wydaje się, że ActionController::RecordNotFoundjest lepsza opcja?
— Peter Ehrlich

4
Kod działał świetnie, ale test nie zadziałał, dopóki nie zdałem sobie sprawy, że korzystam z RSpec 2, który ma inną składnię: expect { visit '/something/you/want/to/404' }.to raise_error(ActionController::RoutingError)/ via stackoverflow.com/a/1722839/993890
— ryanttb

243

Status HTTP 404

Aby zwrócić nagłówek 404, po prostu użyj :statusopcji metody renderowania.

def action
  # here the code

  render :status => 404
end

Jeśli chcesz wyrenderować standardową stronę 404, możesz wyodrębnić funkcję w metodzie.

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

i nazwij to w swoim działaniu

def action
  # here the code

  render_404
end

Jeśli chcesz, aby akcja wyświetlała stronę błędu i zatrzymywała się, po prostu użyj instrukcji return.

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

  # here the code that will never be executed
end

ActiveRecord i HTTP 404

Pamiętaj również, że Railsy ratują niektóre błędy ActiveRecord, takie jak ActiveRecord::RecordNotFound wyświetlanie strony błędu 404.

Oznacza to, że nie musisz samodzielnie ratować tej akcji

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

User.findpodnosi, ActiveRecord::RecordNotFoundgdy użytkownik nie istnieje. To bardzo potężna funkcja. Spójrz na następujący kod

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

Możesz to uprościć, delegując czek do Rails. Po prostu użyj wersji Bang.

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

9
Z tym rozwiązaniem jest duży problem; nadal będzie uruchamiał kod w szablonie. Jeśli więc masz prostą, relaksującą strukturę i ktoś wprowadzi identyfikator, który nie istnieje, szablon będzie szukał obiektu, który nie istnieje.
— jcalvert

5
Jak wspomniano wcześniej, nie jest to poprawna odpowiedź. Spróbuj Stevena.
— Pablo Marambio

Zmieniono wybraną odpowiedź, aby odzwierciedlić lepszą praktykę. Dzięki za komentarze!
— Yuval Karmi

1
Zaktualizowałem odpowiedź, dodając więcej przykładów i notatkę o ActiveRecord.
— Simone Carletti

1
Wersja bang NIE zatrzymuje wykonywania kodu, więc jest to bardziej skuteczne rozwiązanie IMHO.
— Gui vieira,

60

Nowo wybrana odpowiedź przesłana przez Stevena Sorokę jest bliska, ale niepełna. Sam test ukrywa fakt, że nie zwraca prawdziwego 404 - zwraca status 200 - „sukces”. Oryginalna odpowiedź była bliższa, ale próbowała renderować układ tak, jakby nie wystąpiła awaria. To naprawia wszystko:

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

Oto typowy mój zestaw testowy dla czegoś, co spodziewam się zwrócić 404, przy użyciu dopasowań RSpec i 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

Ta zdrowa paranoja pozwoliła mi dostrzec niedopasowanie typu zawartości, gdy wszystko inne wyglądało brzoskwiniowo :) Sprawdzam wszystkie te elementy: przypisane zmienne, kod odpowiedzi, typ treści odpowiedzi, renderowany szablon, renderowany układ, wiadomości flash.

Pominę sprawdzanie typu zawartości w aplikacjach, które są ściśle HTML ... czasami. W końcu „sceptyk sprawdza WSZYSTKIE szuflady” :)

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

FYI: Nie polecam testowania rzeczy, które dzieją się w kontrolerze, tj. „Powinien_jasować”. To, na czym Ci zależy, to wynik. Moje powyższe testy pozwoliły mi wypróbować różne rozwiązania, a testy pozostają takie same, niezależnie od tego, czy rozwiązanie generuje wyjątek, specjalne renderowanie itp.


3
naprawdę podoba mi się ta odpowiedź, szczególnie w odniesieniu do testowania wyjścia, a nie metod wywoływanych w kontrolerze…
— xentek

Szyny ma wbudowane 404 statusu: render :text => 'Not Found', :status => :not_found.
— Lasse Bunk

1
@JaimeBellmyer - jestem pewien, że nie zwraca wartości 200, gdy jesteś w środowisku wdrożonym (tzn. W środowisku pomostowym / produkcyjnym). Robię to w kilku aplikacjach i działa tak, jak opisano w zaakceptowanym rozwiązaniu. Być może to, o czym mówisz, to to, że zwraca 200, gdy renderuje ekran debugowania w fazie rozwoju, w którym prawdopodobnie masz config.consider_all_requests_localustawiony parametr true w swoim environments/development.rbpliku. Jeśli zgłaszasz błąd, jak opisano w przyjętym rozwiązaniu, w inscenizacji / produkcji, na pewno dostaniesz 404, a nie 200.
— Javid Jamae

18

Możesz także użyć pliku renderowania:

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

Gdzie możesz użyć układu, czy nie.

Inną opcją jest użycie wyjątków, aby to kontrolować:

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

13

Wybrana odpowiedź nie działa w Rails 3.1+, ponieważ program obsługi błędów został przeniesiony do oprogramowania pośredniego (patrz problem z github ).

Oto rozwiązanie, z którego jestem zadowolony.

W 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

oraz w application.rb:

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

I w moich zasobach (pokaż, edytuj, aktualizuj, usuń):

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

Można to z pewnością poprawić, ale przynajmniej mam inne widoki dla not_found i internal_error bez nadpisywania podstawowych funkcji Railsów.


3
to bardzo miłe rozwiązanie; jednak nie potrzebujesz tej || not_foundczęści, po prostu zadzwoń find!(zauważ huk), a wyrzuci ActiveRecord :: RecordNotFound, gdy nie będzie można odzyskać zasobu. Dodaj również ActiveRecord :: RecordNotFound do tablicy w warunku if.
— Marek Příhoda

1
Uratowałbym StandardErrori nie na Exceptionwszelki wypadek. Właściwie zostawię standardową stronę statyczną 500 i w ogóle nie render_500rescue_from
— użyję

7

te pomogą ci ...

Kontroler aplikacji

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

Kontroler błędów

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

views / error / 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] %>

po prostu dodaj to do strony, którą chcesz wyrenderować na stronie błędu 404 i gotowe.


1

Chciałem rzucić „normalny” 404 dla każdego zalogowanego użytkownika, który nie jest administratorem, więc ostatecznie napisałem coś takiego w 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

Aby przetestować obsługę błędów, możesz zrobić coś takiego:

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

Jeśli chcesz obsługiwać różne 404 na różne sposoby, rozważ złapanie ich w kontrolerach. Umożliwi to wykonywanie czynności takich jak śledzenie liczby 404 wygenerowanych przez różne grupy użytkowników, wsparcie w interakcji z użytkownikami, aby dowiedzieć się, co poszło nie tak / jaka część doświadczenia użytkownika może wymagać ulepszenia, testowania A / B itp.

Umieściłem tutaj podstawową logikę w ApplicationController, ale można ją również umieścić w bardziej szczegółowych kontrolerach, aby mieć specjalną logikę tylko dla jednego kontrolera.

Powodem, dla którego używam if z ENV [„RESCUE_404”], jest to, że mogę przetestować podnoszenie AR :: RecordNotFound w izolacji. W testach mogę ustawić ten ENV var na false, a mój rescue_from nie będzie strzelał. W ten sposób mogę przetestować podniesienie niezależnie od logiki warunkowej 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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.