web-dev-qa-db-fra.com

rails 4: pages d'erreur personnalisées pour 404, 500 et d'où provient le message d'erreur 500 par défaut?

Actuellement en production je reçois ce texte:

500 Internal Server Error
If you are the administrator of this website, then please read this web application's     
log file and/or the web server's log file to find out what went wrong.

Aucun html dans cette page rien.

Où se situe ce code? J'ai non public/500.html ou quoi que ce soit à cet égard.

Dans mes itinéraires j'ai:

  get "/404", :to => "errors#error_404"
  get "/422", :to => "errors#error_404"
  get "/500", :to => "errors#error_500"
  get "/505", :to => "errors#error_505"

ErrorsController:

class ErrorsController < ApplicationController

  def sub_layout
    "left"
  end

  def error_404
    render :status => 404, :formats => [:html], :layout => "white", :sub_layout => "left"
  end

  def error_422
    render :status => 422, :formats => [:html], :layout => "white", :sub_layout => "left"
  end

  def error_500
    render :status => 500, :formats => [:html], :layout => "white", :sub_layout => "left"
  end

  def error_505
    render :status => 505, :formats => [:html], :layout => "white", :sub_layout => "left"
  end

end

Comment le faire charger mes erreurs personnalisées toujours? Sur certaines erreurs, il suffit de jeter ce texte de 2 lignes provenant quelque part de Rails core, je veux qu'il récupère mes pages d'erreur personnalisées à chaque fois! Comment? Thx!

53
Rubytastic

L'erreur que vous rencontrez est renvoyée

https://github.com/Rails/rails/blob/4-0-stable/actionpack/lib/action_dispatch/middleware/show_exceptions.rb#L18-L22

Cela signifie que le code par lequel vos exceptions sont sauvées est lui-même une exception. Vous pouvez vérifier le texte de vos journaux:

Error during failsafe response:

identifier l'origine des exceptions et résoudre ainsi votre problème.

43
vipulnsward

???? Mise à jour 2018 ????

Notre gemme exception_handler est maintenant considérée comme la plus populaire (pages d'erreur personnalisées Rails)

enter image description hereenter image description here


Comment ça marche

Toutes les Rails sont gérées avec config.exceptions_app . Ceci est attribué dans les fichiers config/application.rb ou config/environments/*.rb - il doit s'agir d'un rappel:

enter image description here

Chaque fois que Rails rencontre une erreur, il appelle le ShowExceptions middleware . Ceci appelle exception_app Et envoie le request entier (y compris exception) au exceptions_app:

Middleware-Powered Exceptions

exceptions_app doit livrer une réponse . Sinon, le failsafe est chargé:

  # show_exceptions.rb#L38
  def render_exception(env, exception)
    wrapper = ExceptionWrapper.new(env, exception)
    status  = wrapper.status_code
    env["action_dispatch.exception"] = wrapper.exception
    env["PATH_INFO"] = "/#{status}"
    response = @exceptions_app.call(request.env) # => exceptions_app callback
    response[1]["X-Cascade"] == "pass" ? pass_response(status) : response
  rescue Exception => failsafe_error # => raised if exceptions_app false
    $stderr.puts "Error during failsafe response: #{failsafe_error}\n  #{failsafe_error.backtrace * "\n  "}"
    FAILSAFE_RESPONSE
  end

Le failsafe est stocké sous le nom FAILSAFE_RESPONSE en haut de ShowExceptions.


Pages d'erreur personnalisées

Si vous voulez créer des pages d'erreur personnalisées, vous devez injecter votre propre rappel dans config.exceptions_app. Cela peut être fait en application ou avec une gemme:

enter image description here

Remarquez comment la méthode call est utilisée - voici comment fonctionne un rappel. Rails (env) est appelé lorsque la demande est reçue d'Internet; lorsqu'une exception est déclenchée, env est passé à exceptions_app .

La qualité de la gestion de vos exceptions dépendra de la façon dont vous gérez env. C'est important; référencer self.routes fait pas reporter l'environnement.

Le meilleur moyen est de gérer les exceptions avec un contrôleur séparé. Cela vous permet de traiter la demande comme s'il ne s'agissait que d'une autre vue, en accordant l'accès au layout et à d'autres composants (model/email).

-

Il y a deux façons de gérer les exceptions:

  1. Redéfinition des itinéraires 404/500
  2. Invoquer un contrôleur

Notre bijou a été conçu autour de notre controller invoqué chaque fois qu'un exception est levé. Cela donne à complet le contrôle du processus d’exception, permettant ainsi une mise en page 100% personnalisée :

enter image description here

ExceptionHandler est à présent le principal joyau de production de pages d'erreur personnalisées pour Rails.

Maintenu pendant plus de 3 ans, c'est le joyau d'exception le plus simple et le plus puissant pour Rails. Cela fonctionne à 100% sur Rails 5 et a déjà été téléchargé plus de 70 000 fois.


Gemme

Le dernier ver (0.8.0.0 a les mises à jour suivantes:

  • Exceptions personnalisées
  • Exception "mappage" (choisissez les exceptions à gérer)
  • Notifications par email
  • Modèle de backend
  • Intégration des pignons 4+
  • Suite de test RSpec
  • Vues basées sur les paramètres régionaux

Vous pouvez en lire plus ici .


Gérer les exceptions de Rails

Si vous n'êtes pas intéressé par la gemme, laissez-moi vous expliquer le processus:

Toutes les exceptions Rails sont gérées avec le rappel ) config.exceptions_app . This est attribué dans les fichiers config/application.rb ou config/environments/*.rb - il doit s'agir d'un rappel:

enter image description here

Chaque fois qu'une exception est générée par votre application, le middleware ShowExceptions est appelé. Ce middleware construit l'exception dans le request et le transmet au rappel config.exceptions_app.

Par défaut, config.exceptions_app Pointe sur les itinéraires. C'est pourquoi Rails est fourni avec 404.html, 500.html Et 422.html Dans le dossier public.

Si vous voulez créer des pages d'exception custom, vous devez écraser le rappel config.exceptions_app - en transmettant la demande erronée à un gestionnaire approprié, que ce soit controller ou route:

[[middleware]]

Les deux manières de gérer cela efficacement sont d’envoyer les requêtes erronées aux routes ou d’appeler un contrôleur.

Le moyen le plus simple - et le plus courant - consiste à transmettre la demande aux itinéraires; malheureusement, cela ignore la demande et vous empêche de détailler correctement les exceptions.

Le meilleur moyen consiste à appeler un contrôleur séparé. Cela vous permettra de passer la demande en entier, de la sauvegarder, de l'envoyer par courrier électronique ou de faire un certain nombre d'autres choses.

-

400/500 Erreurs

Rails peut seulement répondre avec des erreurs HTTP-valides .

Tandis que l'application exception peut être différente, le code d'état renvoyé devrait être 40x Ou 50x. Ceci est conforme aux spécifications HTTP et décrit ici

enter image description here

Cela signifie que quelle que soit la solution de traitement des exceptions que vous utilisez/générez, Rails nécessite pour renvoyer 40x Ou 50x erreurs au navigateur.

En d'autres termes, les pages d'erreur personnalisées ont peu à voir avec le type d'exception - plus comment vous attrapez et servez la réponse du navigateur.

Par défaut, Rails le fait avec les fichiers 404.html, 422.html Et 500.html Dans le dossier public. Si vous le souhaitez pour gérer vous-même le flux des exceptions, vous devez supprimer ces fichiers et canaliser les demandes erronées vers votre propre rappel exceptions_app.

Cela peut être fait avec le routes ou avec un controller (que je vais expliquer maintenant):


1. Routes

Le moyen le plus simple est de laisser les itinéraires le gérer.

Cette méthode est gonflée et nécessite l'utilisation de plusieurs actions. Il est également difficile de gérer les réponses.

Ce tutoriel explique:

enter image description here

Cela montre comment remplacer le exceptions_app Par les routes directement:

# config/application.rb
config.exceptions_app = self.routes

Voici le code que j'ai (Ruby 2.0.0, Rails 4.0)]:

Configuration de l'application

#config/application.rb
config.exceptions_app = self.routes

Itinéraires

#config/routes.rb
if Rails.env.production?
   get '404', to: 'application#page_not_found'
   get '422', to: 'application#server_error'
   get '500', to: 'application#server_error'
end

Contrôleur d'application

#controllers/application_controller.rb
def page_not_found
    respond_to do |format|
      format.html { render template: 'errors/not_found_error', layout: 'layouts/application', status: 404 }
      format.all  { render nothing: true, status: 404 }
    end
  end

  def server_error
    respond_to do |format|
      format.html { render template: 'errors/internal_server_error', layout: 'layouts/error', status: 500 }
      format.all  { render nothing: true, status: 500}
    end
  end

Mise en page des erreurs (totalement statique - pour les erreurs de serveur uniquement)

#views/layouts/error.html.erb
<!DOCTYPE html>
<html>
<head>
  <title><%= action_name.titleize %> :: <%= site_name %></title>
  <%= csrf_meta_tags %>
  <style>
    body {
        background: #fff;
        font-family: Helvetica, Arial, Sans-Serif;
        font-size: 14px;
    }
    .error_container {
        display: block;
        margin: auto;
        margin: 10% auto 0 auto;
        width: 40%;
    }
    .error_container .error {
        display: block; 
        text-align: center;
    }
    .error_container .error img {
        display: block;
        margin: 0 auto 25px auto;
    }
    .error_container .message strong {
        font-weight: bold;
        color: #f00;
    }
  </style>
</head>
<body>

    <div class="error_container">
        <%= yield %>
    </div>

</body>
</html>

Vues d'erreur

#views/errors/not_found_error.html.erb    
<div class="error">
    <h2>Sorry, this page has moved, or doesn't exist!</h2>
</div>


#views/errors/internal_server_error.html.erb
<div class="error">
    <div class="message">
        <strong>Error!</strong>
        We're sorry, but our server is experiencing problems :(
    </div>
</div>

Si beaucoup préfèrent la méthode "routes" pour sa simplicité, elle n’est ni efficace ni modulaire. En effet, si votre application présente un semblant d'orientation d'objet, vous la rejetterez rapidement comme un hack.

Une méthode beaucoup plus efficace consiste à utiliser un contrôleur personnalisé pour intercepter l’exception pure. De cette façon, vous pouvez construire le flux en fonction de la structure globale de votre application:


2. contrôleur

L'autre option consiste à acheminer toutes les demandes vers un contrôleur.

C'est infiniment plus puissant car il vous permet de prendre la requête (exception) et de la transmettre aux vues, tout en la gérant dans le backend. Cela permettra aux utilisateurs de l'enregistrer dans la base de données.

Ce Gist montre comment:

enter image description here

Cela signifie que nous pouvons nous connecter au middleware et transmettre l'ensemble demande à un contrôleur.

Si ce contrôleur est soutenu par un modèle et des vues, nous pouvons l'extraire dans une gemme (ce que nous avons fait). Si vous vouliez le faire manuellement, voici comment:

-

Config

La beauté de cette méthode est qu’elle s’accroche directement à config.exceptions_app . Cela signifie que toute exception peut être gérée de manière native, permettant plus d'efficacité. Pour vous assurer que cela fonctionne, vous devez placer le code suivant dans config/application.rb (exceptions_app Ne fonctionne que dans production - development indique les erreurs):

#config/application.rb
config.exceptions_app = ->(env) { ExceptionController.action(:show).call(env) }

Pour tester, vous pouvez définir les requêtes "locales" sur false:

#config/environments/development.rb
config.consider_all_requests_local  = false # true

-

Contrôleur

L'étape suivante consiste à ajouter un contrôleur exception. Bien que cela puisse être géré dans application_controller, Il est de loin préférable d'extraire dans son propre. Notez l’appel de application.rb - ExceptionController.action(:show):

#app/controllers/exception_controller.rb
class ExceptionController < ApplicationController

  #Response
  respond_to :html, :xml, :json

  #Dependencies
  before_action :status

  #Layout
  layout :layout_status

  ####################
  #      Action      #
  ####################

  #Show
  def show
    respond_with status: @status
  end

  ####################
  #   Dependencies   #
  ####################

  protected

  #Info
  def status
    @exception  = env['action_dispatch.exception']
    @status     = ActionDispatch::ExceptionWrapper.new(env, @exception).status_code
    @response   = ActionDispatch::ExceptionWrapper.rescue_responses[@exception.class.name]
  end

  #Format
  def details
    @details ||= {}.tap do |h|
      I18n.with_options scope: [:exception, :show, @response], exception_name: @exception.class.name, exception_message: @exception.message do |i18n|
        h[:name]    = i18n.t "#{@exception.class.name.underscore}.title", default: i18n.t(:title, default: @exception.class.name)
        h[:message] = i18n.t "#{@exception.class.name.underscore}.description", default: i18n.t(:description, default: @exception.message)
      end
    end
  end
  helper_method :details

  ####################
  #      Layout      #
  ####################

  private

  #Layout
  def layout_status
    @status.to_s == "404" ? "application" : "error"
  end

end

-

Vues

Il y a deux points de vue à ajouter pour que cela fonctionne.

La première est la vue exception/show Et la seconde est la layouts/error. La première consiste à donner une vue au exception_contoller#show, Et la seconde aux erreurs de serveur interne 500.

#app/views/exception/show.html.erb
<h1><%= details[:name]    %></h1>
<p><%=  details[:message] %></p>


#app/views/layouts/error.html.erb (for 500 internal server errors)
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
  <head>
    <title>Error</title>
    <style>
      html {
        height: 100%;
        background: #fff;
      }
      body {
        font-family: Helvetica, Arial, Sans-Serif;
        font-size: 14px;
      }
      .error_container {
        display: block;
        margin: auto;
        margin: 10% auto 0 auto;
        width: 40%;
      }
      .error_container .error {
        display: block;
        text-align: center;
      }
      .error_container .error img {
        display: block;
        margin: 0 auto 15px auto;
      }
      .error_container .message > * {
        display: block;
      }
      .error_container .message strong {
        font-weight: bold;
        color: #f00;
      }
    </style>
  </head>
  <body>
    <div class="error_container"><%= yield %></div>
  </body>
</html>

Conclusion

Le exception importe moins que le code d'erreur.

Lorsque Rails déclenche une exception, il attribue l'un des codes de réponse HTTP ci-dessus. Ils permettent à votre navigateur de déterminer si la demande a abouti.

Lorsque vous traitez avec des exceptions, vous devez vous assurer que vous êtes capable de gérer les erreurs 40* (Qui utilisent généralement la même présentation que le reste de votre application) et les erreurs 50* (Qui nécessiteront leur propre mise en page).

Dans les deux cas, vous ferez mieux d'utiliser un contrôleur exception distinct, qui vous permettra de gérer le exception en tant qu'objet.

69
Richard Peck

Les pages d'erreur dans l'application doivent être aussi simples que possible. La même recommandation concerne leur rendu. Si votre application renvoie 500 codes de réponse HTTP, cela signifie que les choses se sont déjà mal passées. Et il est possible que vous ne puissiez pas afficher la page d'erreur et l'afficher à l'utilisateur.

Dans l'idéal, les pages d'erreur doivent être un code HTML simple servi directement par votre serveur Web, sans toucher le serveur d'applications.

En parlant de Rails implémentation de cette idée. Il est basé sur l’utilisation d’un pipeline d’actifs pour la précompilation des pages statiques HTML.

Premièrement, ajoutez un nouveau type d’actif (Rails> 4.1):

# config/initializers/assets.rb

Rails.application.config.assets.precompile += %w(404.html 500.html)
Rails.application.config.assets.paths << Rails.root.join('app/assets/html')
Rails.application.config.assets.register_mime_type('text/html', '.html')

Si le moteur de gabarit utilise (par exemple, slim, haml), enregistrez-le via un initialiseur:

# for Slim
Rails.application.assets.register_engine('.slim', Slim::Template)
# for Haml
Rails.application.assets.register_engine('.haml', Tilt::HamlTemplate)

Vous êtes maintenant prêt à créer de jolies pages d'erreur dans le répertoire app/assets/html à l'aide de votre moteur de modèle favori et de Rails helpers de vue intégrés.

Conseils pour la production

Le pipeline d’actifs de production ajoute un condensé aux actifs compilés et stocke les fichiers dans un dossier par défaut (généralement partagé/public/actifs sur le serveur de production). Vous pouvez utiliser capistrano pour copier des pages d'erreur à la racine du serveur Web:

# config/deploy.rb
# Capistrano 3 only

namespace :deploy do
  desc 'Copy compiled error pages to public'
  task :copy_error_pages do
    on roles(:all) do
      %w(404 500).each do |page|
        page_glob = "#{current_path}/public/#{fetch(:assets_prefix)}/#{page}*.html"
        # copy newest asset
        asset_file = capture :Ruby, %Q{-e "print Dir.glob('#{page_glob}').max_by { |file| File.mtime(file) }"}
        if asset_file
          execute :cp, "#{asset_file} #{current_path}/public/#{page}.html"
        else
          error "Error #{page} asset does not exist"
        end
      end
    end
  end
  after :finishing, :copy_error_pages
end

Et dernière chose. Indiquez au serveur Web d'utiliser ces fichiers pour certains codes d'erreur HTTP (exemple de configuration nginx):

error_page 500 502 503 504 /500.html;    
error_page 404 /404.html;

Mise à jour du pignon 3

Pour le pignon 3, vous avez besoin de quelque chose comme ceci (testé avec Rails 5)):

# config/environments/production.rb
config.assets.configure do |env|
  env.register_transformer 'text/slim', 'text/html', Slim::Template
  env.register_mime_type 'text/slim', extensions: ['.html']
  env.register_engine '.slim', Slim::Template
end

# config/initializers/assets.rb
Rails.application.config.assets.precompile += %w(404.html 500.html)
Rails.application.config.assets.paths << Rails.root.join('app/assets/html')
17
Darkside