Wie leite ich in Rails zu einem 404 um?

482

Ich möchte eine 404-Seite in Rails "fälschen". In PHP würde ich einfach einen Header mit dem Fehlercode als solchem ​​senden:

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

Wie geht das mit Rails?

Yuval Karmi
quelle

Antworten:

1049

Rendern Sie 404 nicht selbst, es gibt keinen Grund dafür; In Rails ist diese Funktionalität bereits integriert. Wenn Sie eine 404-Seite anzeigen möchten, erstellen Sie eine render_404Methode (oder not_foundwie ich sie nannte) ApplicationControllerwie folgt:

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

Schienen handhaben auch AbstractController::ActionNotFoundund auf ActiveRecord::RecordNotFounddie gleiche Weise.

Dies macht zwei Dinge besser:

1) Es verwendet den in Rails integrierten rescue_fromHandler, um die 404-Seite zu rendern, und 2) es unterbricht die Ausführung Ihres Codes und lässt Sie nette Dinge tun wie:

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

ohne hässliche bedingte Aussagen schreiben zu müssen.

Als Bonus ist es auch in Tests super einfach zu handhaben. Zum Beispiel in einem rspec-Integrationstest:

# 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)

Und am wenigsten:

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

ODER verweisen Sie auf weitere Informationen von Rails Render 404, die von einer Controller-Aktion nicht gefunden wurden

Steven Soroka
quelle
3
Es gibt einen Grund, es selbst zu tun. Wenn Ihre Anwendung alle Routen vom Stamm aus entführt. Es ist schlechtes Design, aber manchmal nicht vermeidbar.
Ablemike
7
Mit diesem Ansatz können Sie auch die ActiveRecord-Bang-Finder (find!, Find_by _...! Usw.) verwenden, die alle eine ActiveRecord :: RecordNotFound-Ausnahme auslösen, wenn kein Datensatz gefunden wird (Auslösen des Rescue_from-Handlers).
Gjvis
2
Dies führt für mich zu einem internen 500-Server-Fehler, nicht zu einem 404. Was fehlt mir?
Glenn
3
Scheint wie ActionController::RecordNotFoundist die bessere Option?
Peter Ehrlich
4
Der Code funktionierte großartig, aber der Test dauerte nicht, bis mir klar wurde, dass ich RSpec 2 mit einer anderen Syntax verwendete: expect { visit '/something/you/want/to/404' }.to raise_error(ActionController::RoutingError)/ via stackoverflow.com/a/1722839/993890
ryanttb
243

HTTP 404 Status

Um einen 404-Header zurückzugeben, verwenden Sie einfach die :statusOption für die Rendermethode.

def action
  # here the code

  render :status => 404
end

Wenn Sie die 404-Standardseite rendern möchten, können Sie das Feature in einer Methode extrahieren.

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

und nenne es in deiner Aktion

def action
  # here the code

  render_404
end

Wenn die Aktion die Fehlerseite rendern und stoppen soll, verwenden Sie einfach eine return-Anweisung.

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

  # here the code that will never be executed
end

ActiveRecord und HTTP 404

Denken Sie auch daran, dass Rails einige ActiveRecord-Fehler behebt, z. B. das ActiveRecord::RecordNotFoundAnzeigen der 404-Fehlerseite.

Sie müssen diese Aktion also nicht selbst retten

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

User.findlöst ein aus, ActiveRecord::RecordNotFoundwenn der Benutzer nicht existiert. Dies ist eine sehr mächtige Funktion. Schauen Sie sich den folgenden Code an

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

Sie können es vereinfachen, indem Sie den Scheck an Rails delegieren. Verwenden Sie einfach die Bang-Version.

def show
  user = User.find_by_email!(params[:email])
  # ...
end
Simone Carletti
quelle
9
Bei dieser Lösung gibt es ein großes Problem. Der Code in der Vorlage wird weiterhin ausgeführt. Wenn Sie also eine einfache, erholsame Struktur haben und jemand eine ID eingibt, die nicht vorhanden ist, sucht Ihre Vorlage nach dem Objekt, das nicht vorhanden ist.
Jcalvert
5
Wie bereits erwähnt, ist dies nicht die richtige Antwort. Probieren Sie Steven's.
Pablo Marambio
Die ausgewählte Antwort wurde geändert, um die bessere Vorgehensweise widerzuspiegeln. Danke für die Kommentare, Leute!
Yuval Karmi
1
Ich habe die Antwort mit weiteren Beispielen und einem Hinweis zu ActiveRecord aktualisiert.
Simone Carletti
1
Die Bang-Version stoppt die Codeausführung, daher ist es meiner Meinung nach die effektivere Lösung.
Gui vieira
60

Die neu ausgewählte Antwort von Steven Soroka ist knapp, aber nicht vollständig. Der Test selbst verbirgt die Tatsache, dass dies keine echte 404 zurückgibt - es gibt einen Status von 200 zurück - "Erfolg". Die ursprüngliche Antwort war näher, versuchte jedoch, das Layout so zu rendern, als wäre kein Fehler aufgetreten. Dies behebt alles:

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

Hier ist ein typischer Testsatz von mir für etwas, von dem ich erwarte, dass er 404 mit RSpec- und Shoulda-Matchern zurückgibt:

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

Diese gesunde Paranoia ermöglichte es mir, die Nichtübereinstimmung des Inhaltstyps zu erkennen, wenn alles andere pfirsichfarben aussah :) Ich überprüfe alle diese Elemente: zugewiesene Variablen, Antwortcode, Antwortinhaltstyp, gerenderte Vorlage, gerendertes Layout, Flash-Nachrichten.

Ich überspringe die Überprüfung des Inhaltstyps für Anwendungen, die ausschließlich HTML sind ... manchmal. Immerhin "überprüft ein Skeptiker ALLE Schubladen" :)

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

Zu Ihrer Information: Ich empfehle nicht, auf Dinge zu testen, die in der Steuerung geschehen, dh "should_raise". Was Sie interessiert, ist die Ausgabe. Bei den oben genannten Tests konnte ich verschiedene Lösungen ausprobieren, und die Tests bleiben gleich, unabhängig davon, ob die Lösung eine Ausnahme, ein spezielles Rendering usw. auslöst.

Jaime Bellmyer
quelle
3
Ich mag diese Antwort wirklich, besonders im Hinblick auf das Testen der Ausgabe und nicht auf die im Controller aufgerufenen Methoden…
Xentek
Rails hat den Status 404 eingebaut : render :text => 'Not Found', :status => :not_found.
Lasse Bunk
1
@JaimeBellmyer - Ich bin sicher , es ist nicht ein 200 zurück , wenn Sie in einer entfalteten (dh Inszenierung / prod) Umwelt sind. Ich mache dies in mehreren Anwendungen und es funktioniert wie in der akzeptierten Lösung beschrieben. Vielleicht beziehen Sie sich darauf, dass beim Rendern des Debug-Bildschirms in der Entwicklung, bei dem der config.consider_all_requests_localParameter in Ihrer environments/development.rbDatei wahrscheinlich auf true gesetzt ist, eine 200 zurückgegeben wird . Wenn Sie einen Fehler, wie in der akzeptierten Lösung beschrieben, in Staging / Produktion
auslösen
18

Sie können auch die Renderdatei verwenden:

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

Wo können Sie wählen, ob Sie das Layout verwenden möchten oder nicht.

Eine andere Möglichkeit besteht darin, die Ausnahmen zu verwenden, um sie zu steuern:

raise ActiveRecord::RecordNotFound, "Record not found."
Paulo Fidalgo
quelle
13

Die ausgewählte Antwort funktioniert in Rails 3.1+ nicht, da der Fehlerbehandler auf eine Middleware verschoben wurde (siehe Github-Problem) ).

Hier ist die Lösung, mit der ich ziemlich zufrieden bin.

In 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

und in application.rb:

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

Und in meinen Ressourcen (anzeigen, bearbeiten, aktualisieren, löschen):

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

Dies könnte sicherlich verbessert werden, aber zumindest habe ich unterschiedliche Ansichten für not_found und internal_error, ohne die Kernfunktionen von Rails zu überschreiben.

Augustin Riedinger
quelle
3
das ist eine sehr schöne Lösung; Sie benötigen das || not_foundTeil jedoch nicht. Rufen Sie einfach auf find!(beachten Sie den Knall), und es wird ActiveRecord :: RecordNotFound ausgelöst, wenn die Ressource nicht abgerufen werden kann. Fügen Sie außerdem ActiveRecord :: RecordNotFound in der if-Bedingung zum Array hinzu.
Marek Příhoda
1
Ich würde retten StandardErrorund nicht Exception, nur für den Fall. Eigentlich werde ich die statische Standard-500-Seite verlassen und überhaupt keine benutzerdefinierten Seiten verwenden render_500, was bedeutet, dass ich explizit eine rescue_fromReihe von Fehlern im Zusammenhang mit 404
Dr.Strangelove
7

diese werden dir helfen ...

Anwendungscontroller

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

Fehler Controller

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

Ansichten / Fehler / Fehler_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
quelle
3
<%= render file: 'public/404', status: 404, formats: [:html] %>

Fügen Sie dies einfach zu der Seite hinzu, die Sie auf der 404-Fehlerseite rendern möchten, und fertig.

Ahmed Reza
quelle
1

Ich wollte einen "normalen" 404 für jeden angemeldeten Benutzer werfen, der kein Administrator ist, also schrieb ich am Ende so etwas in 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
leere Wände
quelle
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
quelle
0

Um die Fehlerbehandlung zu testen, können Sie Folgendes tun:

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
quelle
0

Wenn Sie mit verschiedenen 404 auf unterschiedliche Weise umgehen möchten, sollten Sie sie in Ihren Controllern abfangen. Auf diese Weise können Sie beispielsweise die Anzahl der von verschiedenen Benutzergruppen generierten 404s verfolgen, den Support mit Benutzern interagieren lassen, um herauszufinden, was schief gelaufen ist / welcher Teil der Benutzererfahrung möglicherweise optimiert werden muss, A / B-Tests durchführen usw.

Ich habe hier die Basislogik in ApplicationController platziert, aber sie kann auch in spezifischeren Controllern platziert werden, um nur für einen Controller eine spezielle Logik zu haben.

Der Grund, warum ich ein if mit ENV ['RESCUE_404'] verwende, ist, dass ich das Auslösen von AR :: RecordNotFound isoliert testen kann. In Tests kann ich diese ENV-Variable auf false setzen, und meine Rettung_von würde nicht ausgelöst. Auf diese Weise kann ich das Erhöhen getrennt von der bedingten 404-Logik testen.

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
quelle