Jak przekierować na 404 w Railsach?

482

Chciałbym „sfałszować” stronę 404 w Railsach. W PHP po prostu wysyłam nagłówek z kodem błędu jako takim:

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

Jak to się robi z Railsami?

Yuval Karmi
źródło

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

Steven Soroka
źródło
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
Simone Carletti
źródło
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.

Jaime Bellmyer
źródło
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."
Paulo Fidalgo
źródło
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.

Augustin Riedinger
źródło
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
Caner Çakmak
źródło
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.

Ahmed Reza
źródło
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
puste ściany
źródło
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
Arkadiusz Mazur
źródło
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
Marek Příhoda
źródło
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
Houen
źródło