Handling Exceptions

Handling Exceptions

Standardmäßig rendert Rails im Produktionsmodus eine statische Fehlerdatei, wenn eine Ausnahme auftritt. Hier erfahren Sie, wie Sie dieses Verhalten vollständig anpassen können, um dynamische Fehlerseiten zu rendern.

App erstellen:

$ rails new HandlingExceptions
$ cd HandlingExceptions
$ rails g scaffold Product name price:decimal
$ rails db:migrate
Ein gem installieren
// Gemfile

gem 'sassc-rails'

$ bundle install
Ausnahmen treten in einer Rails-Anwendung zwangsläufig auf, entweder aufgrund eines Fehlers im Code unserer Anwendung oder auf Seiten des Benutzers. Rails behandelt beide auf ähnliche Weise: In der Entwicklungsumgebung zeigt es die vertraute Debug-Seite an, um uns bei der Lösung dessen zu helfen, was wir falsch gemacht haben. In der Produktionsumgebung wird eine statische Fehlerseite gerendert. Wir können die Produktionsumgebung in der Entwicklung simulieren, indem wir die Entwicklungskonfigurationsdatei ändern und eine der Einstellungen anpassen.
// config/environments/development.rb

config.consider_all_requests_local = false
Nachdem wir unsere Rails-Anwendung neu gestartet und eine Ausnahme ausgelöst haben, sehen wir die statische Fehlermeldungsseite, als ob unsere Anwendung sich in der Produktionsumgebung befände.

1.png 607 KB


Übrigens sollten wir, bevor wir eine Anwendung bereitstellen, die statischen Fehlerseiten aktualisieren, um besser zum Design zu passen, da dies leicht vergessen werden kann. Diese Seiten finden wir im Verzeichnis /public. Standardmäßig werden drei verschiedene Statuscodes behandelt, aber wir können problemlos weitere hinzufügen, um andere Fälle zu behandeln. Wir erstellen als erstes eine Layoutseite error.html.erb
// app/views/layouts/error.html.erb

<!DOCTYPE html>
<html>

  <head>
    <title>HandlingExceptions</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= javascript_importmap_tags %>
  </head>

  <body>
    <main id="container">
      <%= yield %>
    </main>
  </body>

</html>
Angenommen, wir haben einen bestimmten Typ von Ausnahme, für den wir diese Fehlermeldung anzeigen möchten. In unserem ProductsController haben wir eine benutzerdefinierte Forbidden-Klasse definiert, die von StandardError erbt, und diese wird ausgelöst, wenn jemand die show-Aktion aufruft. Der Besuch dieser Seite löst derzeit einen 500-Fehler aus, Um dieses Verhalten zu ändern, müssen wir die Konfigurationsdatei der Anwendung ändern.
// app/config/application.rb

class Application < Rails::Application
  # ...

  config.action_dispatch.rescue_responses["ProductsController::Forbidden"] = :forbidden
end
Hier setzen wir config.action_dispatch.rescue_responses, das eine Hash-Tabelle ist, wobei der Schlüssel den Namen der Ausnahme repräsentiert, die wir behandeln möchten, und der Wert der Statuscode ist, in diesem Fall :forbidden (wir können die Statuscodes verwenden, die Rack anstelle eines numerischen HTTP-Status verwendet). Um mehr darüber zu erfahren, wie Rack Statuscodes behandelt, werfen Sie einen Blick in die Dokumentation für Rack::Utils, wo Sie die Namen für jeden Statuscode und wie er in ein Symbol umgewandelt wird, finden.

Als Nächstes zeigen wir Ihnen einige der Ausnahmen, die Rails standardmäßig zuordnet. Wenn wir beispielsweise eine Route auslösen, die nicht existiert, sehen wir einen 404-Fehler anstelle eines 500-Fehlers. Die meisten dieser Ausnahmen sind in der Rails-Quellcodebasis in der Klasse ExceptionWrapper definiert. Diese Klasse legt die Standard-rescue_responses-Hash-Tabelle fest, die wir zuvor konfiguriert haben, und einer der festgelegten Werte ist ActionController::RoutingError, der auf :not_found gesetzt ist. Dies ist das, was wir in der Anwendung mit dem Status 404 sehen.

Dynamische Fehlerseiten

Das ist alles gut und schön, aber was ist, wenn wir etwas Dynamischeres in unseren Fehlerseiten haben möchten? Wir zeigen derzeit statische Inhalte an, aber was ist, wenn wir die Nachricht in etwas ändern möchten, das spezifischer für die Situation des Benutzers ist? Eine Möglichkeit dies zu tun, besteht über den Controller. Wir können die Methode rescue_from aufrufen, um das Verhalten bestimmter Ausnahmen zu überschreiben.
// app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  protect_from_forgery

  rescue_from 'ProductsController::Forbidden', with: :forbidden

  private

  def forbidden(exception)
    render plain: exception.message
  end
end

// app/controllers/products_controller.rb
  
# GET /products/1 or /products/1.json
  def show
    raise Forbidden, 'You are not allowed to access this product.'
  end
Hier rufen wir eine forbidden-Methode auf, wenn unsere ProductsController::Forbidden-Ausnahme ausgelöst wird. Um die Dinge einfach zu halten, rendert diese Methode einfach den Text der ausgelösten Ausnahme, damit wir das dynamische Verhalten sehen können. Wenn wir nun die Seite neu laden, die diese Ausnahme auslöst, sehen wir unseren benutzerdefinierten Fehler.

Wir könnten stattdessen hier eine Vorlage rendern oder eine Weiterleitung mit einer Flash-Meldung durchführen, und dies ist ein guter Anwendungsfall für die Verwendung von rescue_from, wenn wir eine benutzerdefinierte definierte Ausnahme behandeln. Wenn wir jedoch jede Art von Ausnahme auf diese Weise in eine dynamische Fehlerseite umwandeln möchten, hat rescue_from Einschränkungen. Um dies zu demonstrieren, werden wir versuchen, jede Art von Ausnahme auf diese Weise zu behandeln.
// app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  protect_from_forgery

  rescue_from 'Exception', with: :forbidden

  private

  def forbidden(exception)
    render plain: exception.message
  end
end
Wenn wir die Seite neu laden, funktioniert es wie zuvor, und wenn wir versuchen, die Seite für ein Produkt zu besuchen, das nicht existiert, sehen wir den Text für eine Forbidden-Ausnahme. Wenn wir eine Seite besuchen, für die keine Route vorhanden ist, wird eine Ausnahme ausgelöst, und diese wird vom Controller nicht behandelt, da sie außerhalb der Controller-Schicht ausgelöst wird. Es gibt mehrere andere Gründe, warum wir rescue_from möglicherweise nicht verwenden möchten: Es überschreibt das Verhalten der Ausnahme vollständig, sodass wir überprüfen müssen, ob wir uns in der Entwicklung oder Produktion befinden. Wenn wir versuchen, dies auf HTTP-Statuscodes zurückzubilden, ist dies von hier aus schwierig, da es so früh im Prozess erfolgt. Außerdem löst dies keine Benachrichtigungs-E-Mails oder Warnungen für Ausnahmen aus, die wir möglicherweise eingerichtet haben. Im Allgemeinen ist dies nicht der beste Ansatz, wenn wir versuchen, Fehlerseiten dynamisch zu gestalten.

Behandlung von Ausnahmen mit Middleware

Es ist besser, dies stattdessen über Rack-Middleware zu behandeln. Eine Middleware für unsere Anwendung ist beispielsweise ActionDispatch::ShowExceptions, die das Auffangen von Ausnahmen behandelt und den Fehler rendert. Wenn wir uns den Quellcode dafür ansehen, werden wir feststellen, dass sein Initialisierer etwas namens exceptions_app akzeptiert, das eine Rack-App ist, an die die Behandlung von Ausnahmen delegiert wird. Standardmäßig delegiert es an etwas namens PublicExceptions, das eine in Rails eingebaute Rack-App ist und die statischen HTML-Fehlerseiten rendert. Wenn wir hier stattdessen etwas Dynamisches tun möchten, können wir unsere eigene Rack-App definieren und diese als exceptions_app bereitstellen. Dazu müssen wir nur die Konfigurationsdatei unserer Anwendung ändern und die Konfigurationsoption exceptions_app festlegen. Anstelle einer benutzerdefinierten Rack-App zur Handhabung können wir unsere Rails-App selbst verwenden. Wir können dies tun, indem wir routes als exceptions_app definieren.
// config/application.rb

config.exceptions_app = routes
Um dies zum Laufen zu bringen, müssen wir unsere Routen konfigurieren, um mit den verschiedenen Fehlerstatuscodes umzugehen. Der Fehlercode wird als Pfad übergeben, sodass wir ihn entsprechend unseren Anforderungen abgleichen können. Zum Beispiel könnten wir alle 404-Fehler auf die Startseite umleiten.
// config/routes.rb

Rails.application.routes.draw do
  get 'up' => 'rails/health#show', as: :rails_health_check

  resources :products
  root to: 'products#index'
  get '404', to: redirect('/')
end
Jetzt, wenn wir versuchen, eine Seite zu besuchen, die nicht existiert, werden wir zur Startseite umgeleitet. Dies ist wirklich leistungsstark, da wir anstelle einer Weiterleitung jede Controller-Aktion auslösen können. Wir werden einen neuen Controller dafür generieren, den wir Errors nennen werden.

$ rails g controller Errors
Wir könnten dies verwenden, um 404-Fehler zu einer not_found-Aktion umzuleiten, aber stattdessen werden wir die Route allgemeiner gestalten und alle Statuscodes zur Aktion des Controllers leiten. Wir werden Einschränkungen verwenden, um sicherzustellen, dass nur Pfade, die aus drei Ziffern bestehen, übereinstimmen.
// config/routes.rb

  get '404', to: 'errors#not_found'
  get '500', to: 'errors#internal_server'
  get '422', to: 'errors#unprocessable'
  get '406', to: 'errors#unacceptable'
Wir können nun die Aktion des ErrorsController definieren. Im Moment werden wir nur den HTTP-Status anzeigen, und obwohl wir erwarten könnten, dies durch Rendern von params[:status] tun zu können, werden die Parameter tatsächlich von der Controller-Aktion übernommen, die den Fehler verursacht hat.
// app/controllers/errors_controller.rb

class ErrorsController < ApplicationController
  before_action :exeption, only: %i[not_found internal_server unprocessable unacceptable]
  layout 'error'

  def not_found
    render status: 404
  end

  def internal_server
    render status: 500
  end

  def unprocessable
    render status: 422
  end

  def unacceptable
    render status: 406
  end

  private

  def exeption
    @status = params[:status]
    @exception = request.env['action_dispatch.exception']
  end
end
Jetzt wird eine Ausnahme durch den ErrorsController behandelt und der Status wird angezeigt.

Wir können jetzt eine dynamische Vorlage für einen beliebigen Fehler rendern. Wenn wir vier Ansichtsvorlagen unter /app/views/errors erstellen, können wir diese verwenden, um eine für den Status relevante Nachricht anzuzeigen.
// app/views/errors/internal_server.html.erb

<h1>Forbidden.</h1>
<% if @exception %>
  <p><%= @exception.message %></p>
<% else %>
  <p>You are not authorized to perform that action.</p>
<% end %>

// app/views/errors/not_found.html.erb

<h1>Not Found</h1>
<p>The page you are looking for doesn't exist.</p>

2.png 621 KB



// app/views/errors/unprocessable.html.erb

<h1>Change Rejected</h1>
<p>Maybe you tried to change something you didn't have access to.</p>

// app/views/errors/unacceptable.html.erb

<h1>Forbidden.</h1>
<% if @exception %>
<p><%= @exception.message %></p>
<% else %>
<p>You are not authorized to perform that action.</p>
<% end %>

3.png 629 KB



Wir werden diese aus dem Controller aufrufen und den anfänglichen Schrägstrich vom Pfad entfernen, damit die Vorlage basierend auf der Statusnummer aufgerufen wird.

Wenn wir jetzt eine nicht vorhandene Seite besuchen, sehen wir die Vorlage für den Status 404.Wir können dies dann verwenden, um die Nachricht der Ausnahme anzuzeigen, sofern vorhanden.

Jetzt wird bei Auslösung einer Ausnahme die spezifische Fehlermeldung angezeigt. 

Das ist großartig. Wir können jetzt Ausnahmefehlermeldungen dynamisch behandeln, und das gibt uns viele Möglichkeiten. Dennoch ist es am besten, hier nicht zu übertreiben und besonders darauf zu achten, dass wir keine Ausnahme auslösen, wenn wir die Seite behandeln, auf der entschieden wird, was bei einer Ausnahme zu tun ist.

Auch wenn wir solche dynamischen Seiten haben, ist es eine gute Idee, die statischen Versionen im /public-Verzeichnis auf dem neuesten Stand zu halten und sie an das Design unserer Anwendung anzupassen, falls sie vom Webserver selbst verwendet werden. Wir können dies tun, indem wir sie von unseren dynamischen Seiten generieren. Wenn wir dies tun, sollten wir die Route ändern, die die Fehlerseiten behandelt, damit sie einen optionalen Fehlerpfad unterstützt, da sie sonst versuchen wird, die öffentliche Seite zu verwenden.

Mit diesem Ansatz können wir die statischen Fehlerseiten jederzeit neu generieren, indem wir eine Anfrage an die dynamische Fehlerseite stellen und die Ausgabe an eine statische Fehlerseite weiterleiten.

Wichtig! Die Fehlerseiten aus dem Public-Ordner raus löschen.
Meld dich an und schreibe ein Kommentar