Comment rediriger vers un 404 dans Rails?

482

Je voudrais «simuler» une page 404 dans Rails. En PHP, je voudrais simplement 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?

Yuval Karmi
la source

Réponses:

1049

Ne restituez pas 404 vous-même, il n'y a aucune raison de le faire; Rails a déjà cette fonctionnalité intégrée. Si vous voulez afficher une page 404, créez une render_404méthode (ou not_foundcomme je l'ai appelée) ApplicationControllercomme ceci:

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

Rails gère également AbstractController::ActionNotFound, et de ActiveRecord::RecordNotFoundla même manière.

Cela fait deux choses mieux:

1) Il utilise le rescue_fromgestionnaire intégré de Rails pour rendre la page 404, et 2) il interrompt l'exécution de votre code, vous permettant de faire de belles choses comme:

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

sans avoir à écrire de vilaines instructions conditionnelles.

En prime, il est également très facile à manipuler 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 référez-vous à plus d'informations sur le rendu Rails 404 introuvable à partir d'une action du contrôleur

Steven Soroka
la source
3
Il y a une raison de le faire vous-même. Si votre application détourne tous les itinéraires depuis la racine. C'est un mauvais design, mais parfois inévitable.
ablemike
7
Cette approche vous permet également d'utiliser les détecteurs de bang ActiveRecord (find !, find_by _...!, Etc.), qui déclenchent tous une exception ActiveRecord :: RecordNotFound si aucun enregistrement n'est trouvé (déclenchant le gestionnaire rescue_from).
gjvis
2
Cela soulève une erreur de serveur interne 500 pour moi, pas un 404. Qu'est-ce qui me manque?
Glenn
3
On dirait que ActionController::RecordNotFoundc'est la meilleure option?
Peter Ehrlich
4
Le code a très bien fonctionné, mais le test n'a pas fonctionné jusqu'à ce que je réalise que j'utilise RSpec 2 qui a une syntaxe différente: expect { visit '/something/you/want/to/404' }.to raise_error(ActionController::RoutingError)/ via stackoverflow.com/a/1722839/993890
ryanttb
243

Statut HTTP 404

Pour renvoyer un en-tête 404, utilisez simplement l' :statusoption de la méthode de rendu.

def action
  # here the code

  render :status => 404
end

Si vous souhaitez rendre la page 404 standard, vous pouvez extraire la fonction 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 affiche la page d'erreur et s'arrête, utilisez simplement une instruction de retour.

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

  # here the code that will never be executed
end

ActiveRecord et HTTP 404

N'oubliez pas non plus que Rails récupère certaines erreurs ActiveRecord, telles que l' ActiveRecord::RecordNotFoundaffichage de 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.finddéclenche un ActiveRecord::RecordNotFoundlorsque l'utilisateur n'existe pas. Il s'agit d'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
Simone Carletti
la source
9
Il y a un gros problème avec cette solution; il exécutera toujours le code dans le modèle. Donc, si vous avez une structure simple et reposante et que quelqu'un entre un ID qui n'existe pas, votre modèle recherchera l'objet qui n'existe pas.
jcalvert
5
Comme mentionné précédemment, ce n'est pas la bonne réponse. Essayez celui de Steven.
Pablo Marambio
Modification de la réponse sélectionnée pour refléter la meilleure pratique. Merci pour les commentaires, les gens!
Yuval Karmi
1
J'ai mis à jour la réponse avec plus d'exemples et une note sur ActiveRecord.
Simone Carletti
1
La version Bang arrête l'exécution du code, c'est donc la solution la plus efficace à mon humble avis.
Gui vieira
60

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 renvoie pas un vrai 404 - il renvoie un statut de 200 - "succès". La réponse d'origine était plus proche, mais a tenté de rendre la mise en page comme si aucune défaillance ne s'était produite. Cela corrige tout:

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

Voici un ensemble de test typique de la mienne pour quelque chose que je m'attends à retourner 404, en utilisant des 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 paranoïa saine m'a permis de repérer l'inadéquation du type de contenu lorsque tout le reste avait l'air pêche :) Je vérifie tous ces éléments: variables attribuées, code de réponse, type de contenu de la réponse, modèle rendu, mise en page rendue, messages flash.

Je vais sauter 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 les choses qui se produisent dans le contrôleur, c'est-à-dire "should_raise". Ce qui vous intéresse, c'est la sortie. Mes tests ci-dessus m'ont permis d'essayer différentes solutions, et les tests restent les mêmes que la solution lève une exception, un rendu spécial, etc.

Jaime Bellmyer
la source
3
aime vraiment cette réponse, surtout en ce qui concerne le test de la sortie et non les méthodes appelées dans le contrôleur ...
xentek
Rails a intégré statut 404: render :text => 'Not Found', :status => :not_found.
Lasse Bunk
1
@JaimeBellmyer - Je suis certain qu'il ne retourne pas 200 lorsque vous êtes dans un environnement déployé (c'est-à-dire staging / prod). Je le fais dans plusieurs applications et cela fonctionne comme décrit dans la solution acceptée. Peut-être que vous faites référence à cela, il retourne un 200 lorsqu'il rend l'écran de débogage en développement où vous avez probablement le config.consider_all_requests_localparamètre défini sur true dans votre environments/development.rbfichier. Si vous soulevez une erreur, comme décrit dans la solution acceptée, dans la mise en scène / production, vous obtiendrez certainement un 404, pas un 200.
Javid Jamae
18

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."
Paulo Fidalgo
la source
13

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 github ).

Voici la solution que j'ai trouvée et qui me satisfait.

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 des vues différentes pour not_found et internal_error sans remplacer les fonctions principales de Rails.

Augustin Riedinger
la source
3
c'est une très bonne solution; cependant, vous n'avez pas besoin de la || not_foundpartie, il suffit d'appeler find!(remarquez le coup) et il lancera ActiveRecord :: RecordNotFound lorsque la ressource ne peut pas être récupérée. Ajoutez également ActiveRecord :: RecordNotFound au tableau dans la condition if.
Marek Příhoda
1
Je sauverais StandardErroret pas Exception, juste au cas où. En fait, je vais laisser une page statique standard de 500 et ne pas utiliser du tout de coutume render_500, ce qui signifie que je vais explicitement mettre en rescue_fromréseau des erreurs liées à 404
Dr.Strangelove
7

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

vues / erreurs / 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
la source
3
<%= render file: 'public/404', status: 404, formats: [:html] %>

il suffit d'ajouter cela à la page que vous souhaitez afficher sur la page d'erreur 404 et vous avez terminé.

Ahmed Reza
la source
1

Je voulais lancer un 404 `` normal '' pour tout utilisateur connecté qui n'est pas un administrateur, j'ai donc fini par écrire quelque chose comme ça 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
murs vides
la source
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
la source
0

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
Marek Příhoda
la source
0

Si vous souhaitez gérer différents 404 de différentes manières, pensez à les attraper dans vos contrôleurs. Cela vous permettra de suivre le nombre de 404 générés par différents groupes d'utilisateurs, de faire en sorte que le support interagisse avec les utilisateurs pour découvrir ce qui ne va pas / quelle partie de l'expérience utilisateur pourrait avoir besoin d'être modifiée, faire des tests A / B, etc.

J'ai placé ici la logique de base dans ApplicationController, mais elle peut également être placée dans des contrôleurs plus spécifiques, pour avoir une logique spéciale uniquement pour un contrôleur.

La raison pour laquelle j'utilise un if avec ENV ['RESCUE_404'], c'est pour que je puisse tester l'augmentation de AR :: RecordNotFound de manière isolée. Dans les tests, je peux définir cette var ENV sur false, et mon rescue_from ne se déclencherait pas. De cette façon, je peux tester l'élévation séparément de la logique 404 conditionnelle.

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
la source