J'aimerais "simuler" une page 404 dans Rails. En PHP, je voudrais juste envoyer un en-tête avec le code d'erreur en tant que tel:
header("HTTP/1.0 404 Not Found");
Comment cela se fait-il avec Rails?
Ne rendez pas 404 vous-même, il n'y a aucune raison de; Rails a déjà cette fonctionnalité intégrée. Si vous souhaitez afficher une page 404, créez une méthode render_404
(ou not_found
comme je l'ai appelée) dans ApplicationController
comme ceci:
def not_found
raise ActionController::RoutingError.new('Not Found')
end
Rails gère également AbstractController::ActionNotFound
et ActiveRecord::RecordNotFound
de la même manière.
Cela fait mieux deux choses:
1) Il utilise le gestionnaire rescue_from
intégré de Rails pour restituer la page 404, et 2) il interrompt l'exécution de votre code, vous permettant ainsi de faire des choses agréables comme:
user = User.find_by_email(params[:email]) or not_found
user.do_something!
sans avoir à écrire de mauvaises déclarations conditionnelles.
En prime, il est également très facile à gérer dans les tests. Par exemple, dans un test d'intégration 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)
Et minitest:
assert_raises(ActionController::RoutingError) do
get '/something/you/want/to/404'
end
OU se référer à plus d'informations à partir de Rails rendent 404 introuvable à partir d'une action du contrôleur
Pour retourner un en-tête 404, utilisez simplement l'option :status
pour la méthode de rendu.
def action
# here the code
render :status => 404
end
Si vous souhaitez afficher la page 404 standard, vous pouvez extraire la fonctionnalité dans une méthode.
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
et appelez-le dans votre action
def action
# here the code
render_404
end
Si vous souhaitez que l'action rende la page d'erreur et s'arrête, utilisez simplement une instruction return.
def action
render_404 and return if params[:something].blank?
# here the code that will never be executed
end
N'oubliez pas non plus que Rails sauve certaines erreurs ActiveRecord, telles que le ActiveRecord::RecordNotFound
affichant la page d'erreur 404.
Cela signifie que vous n'avez pas besoin de sauver cette action vous-même
def show
user = User.find(params[:id])
end
User.find
lève un ActiveRecord::RecordNotFound
lorsque l'utilisateur n'existe pas. C'est une fonctionnalité très puissante. Regardez le code suivant
def show
user = User.find_by_email(params[:email]) or raise("not found")
# ...
end
Vous pouvez le simplifier en déléguant à Rails le chèque. Utilisez simplement la version bang.
def show
user = User.find_by_email!(params[:email])
# ...
end
La réponse nouvellement sélectionnée soumise par Steven Soroka est proche mais pas complète. Le test lui-même cache le fait que cela ne retourne pas un véritable 404 - il renvoie un statut de 200 - "succès". La réponse initiale était plus proche, mais a tenté de rendre la mise en page comme si aucune erreur ne s'était produite. Cela corrige tout:
render :text => 'Not Found', :status => '404'
Voici un ensemble typique de tests pour quelque chose que je m'attends à rendre 404, en utilisant les matchers RSpec et 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
Cette saine paranoïa m'a permis de repérer l'inadéquation de type de contenu lorsque tout le reste semblait peachy :) Je vérifie tous ces éléments: variables affectées, code de réponse, type de contenu de la réponse, modèle rendu, mise en forme, messages flash.
Je vais ignorer la vérification du type de contenu sur les applications strictement HTML ... parfois. Après tout, "un sceptique vérifie TOUS les tiroirs" :)
http://dilbert.com/strips/comic/1998-01-20/
FYI: Je ne recommande pas de tester ce qui se passe dans le contrôleur, c'est-à-dire "should_raise". Ce qui compte pour vous, c'est la sortie. Mes tests ci-dessus m'ont permis d'essayer diverses solutions, et les tests restent les mêmes, que la solution soulève une exception, un rendu spécial, etc.
Vous pouvez également utiliser le fichier de rendu:
render file: "#{Rails.root}/public/404.html", layout: false, status: 404
Où vous pouvez choisir d'utiliser la mise en page ou non.
Une autre option consiste à utiliser les exceptions pour le contrôler:
raise ActiveRecord::RecordNotFound, "Record not found."
La réponse sélectionnée ne fonctionne pas dans Rails 3.1+ car le gestionnaire d'erreurs a été déplacé vers un middleware (voir problème de github ).
Voici la solution que j'ai trouvée et dont je suis assez content.
Dans 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
et dans application.rb
:
config.after_initialize do |app|
app.routes.append{ match '*a', :to => 'application#not_found' } unless config.consider_all_requests_local
end
Et dans mes ressources (afficher, éditer, mettre à jour, supprimer):
@resource = Resource.find(params[:id]) or not_found
Cela pourrait certainement être amélioré, mais au moins, j'ai différentes vues pour not_found et internal_error sans surcharger les fonctions principales Rails.
ceux-ci vous aideront ...
Contrôleur d'application
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
Contrôleur d'Erreurs
class ErrorsController < ApplicationController
def error_404
@not_found_path = params[:not_found]
end
end
views/errors/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
Je voulais lancer un "normal" 404 pour tout utilisateur connecté qui n'est pas un administrateur, alors j'ai fini par écrire quelque chose comme ceci dans 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
<%= render file: 'public/404', status: 404, formats: [:html] %>
ajoutez simplement ceci à la page que vous voulez rendre à la page d'erreur 404 et vous avez terminé.
Pour tester la gestion des erreurs, vous pouvez faire quelque chose comme ceci:
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
Si vous souhaitez gérer différents 404 de différentes manières, pensez à les attraper dans vos contrôleurs. Cela vous permettra notamment de suivre le nombre de 404 générés par différents groupes d’utilisateurs, de faire interagir l’assistance avec les utilisateurs pour savoir ce qui ne va pas/quelle partie de l’expérience utilisateur peut nécessiter d’être peaufinée, effectuer des tests A/B, etc.
J'ai ici placé la logique de base dans ApplicationController, mais elle peut également être placée dans des contrôleurs plus spécifiques, pour ne disposer que d'une logique spéciale pour un seul contrôleur.
La raison pour laquelle j'utilise un if avec ENV ['RESCUE_404'], c'est pour pouvoir tester la montée de AR :: RecordNotFound de manière isolée. Lors des tests, je peux définir la valeur de cette variable ENV sur false et mon rescue_from ne se déclenche pas. De cette façon, je peux tester la levée séparément de la logique conditionnelle 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
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