Come reindirizzare a un 404 in Rails?


482

Vorrei "falsificare" una pagina 404 in Rails. In PHP, vorrei semplicemente inviare un'intestazione con il codice di errore in quanto tale:

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

Come si fa con Rails?

Risposte:


1049

Non renderti 404 te stesso, non c'è motivo di farlo; Rails ha già questa funzionalità integrata. Se vuoi mostrare una pagina 404, crea un render_404metodo (o not_foundcome l'ho chiamato) in ApplicationControllerquesto modo:

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

Rails gestisce anche AbstractController::ActionNotFound, e ActiveRecord::RecordNotFoundallo stesso modo.

Questo fa due cose meglio:

1) Utilizza il rescue_fromgestore integrato di Rails per eseguire il rendering della pagina 404 e 2) interrompe l'esecuzione del codice, permettendoti di fare cose carine come:

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

senza dover scrivere brutte dichiarazioni condizionali.

Come bonus, è anche super facile da gestire nei test. Ad esempio, in un test di integrazione 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)

E più minuscolo:

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

OPPURE fare riferimento a maggiori informazioni da Rails render 404 non trovato da un'azione del controller


3
C'è un motivo per farlo da soli. Se l'applicazione dirotta tutti i percorsi dalla radice. È un cattivo design, ma a volte non evitabile.
ablemike,

7
Questo approccio consente inoltre di utilizzare i rilevatori di bang di ActiveRecord (find !, find_by _...!, Ecc.), Che generano tutti un'eccezione ActiveRecord :: RecordNotFound se non viene trovato alcun record (attivando il gestore rescue_from).
gjvis,

2
Ciò genera un errore del server interno 500 per me, non un 404. Cosa mi sto perdendo?
Glenn,

3
Sembra che ActionController::RecordNotFoundsia l'opzione migliore?
Peter Ehrlich,

4
Il codice ha funzionato grande, ma il test non ha fatto fino a quando mi sono reso conto che stavo usando RSpec 2 che ha una sintassi diversa: expect { visit '/something/you/want/to/404' }.to raise_error(ActionController::RoutingError)/ via stackoverflow.com/a/1722839/993890
ryanttb

243

Stato HTTP 404

Per restituire un'intestazione 404, basta usare l' :statusopzione per il metodo di rendering.

def action
  # here the code

  render :status => 404
end

Se si desidera eseguire il rendering della pagina 404 standard, è possibile estrarre la funzione in un metodo.

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

e chiamalo nella tua azione

def action
  # here the code

  render_404
end

Se vuoi che l'azione renda la pagina di errore e si fermi, usa semplicemente un'istruzione return.

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

  # here the code that will never be executed
end

ActiveRecord e HTTP 404

Ricorda inoltre che Rails salva alcuni errori ActiveRecord, come la ActiveRecord::RecordNotFoundvisualizzazione della pagina di errore 404.

Significa che non è necessario salvare questa azione da soli

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

User.findgenera un ActiveRecord::RecordNotFoundquando l'utente non esiste. Questa è una funzionalità molto potente. Guarda il seguente codice

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

Puoi semplificarlo delegando a Rails il controllo. Usa semplicemente la versione bang.

def show
  user = User.find_by_email!(params[:email])
  # ...
end

9
C'è un grosso problema con questa soluzione; eseguirà comunque il codice nel modello. Quindi se hai una struttura semplice e riposante e qualcuno inserisce un ID che non esiste, il tuo modello cercherà l'oggetto che non esiste.
jcalvert,

5
Come accennato in precedenza, questa non è la risposta corretta. Prova Steven's.
Pablo Marambio,

Modificata la risposta selezionata per riflettere la migliore pratica. Grazie per i commenti ragazzi!
Yuval Karmi,

1
Ho aggiornato la risposta con più esempi e una nota su ActiveRecord.
Simone Carletti,

1
La versione bang interrompe l'esecuzione del codice, quindi è la soluzione più efficace IMHO.
Gui vieira,

60

La risposta appena selezionata inviata da Steven Soroka è vicina, ma non completa. Il test stesso nasconde il fatto che questo non sta restituendo un vero 404 - sta restituendo uno stato di 200 - "successo". La risposta originale era più vicina, ma tentò di renderizzare il layout come se non si fosse verificato alcun errore. Questo risolve tutto:

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

Ecco un mio tipico set di test per qualcosa che mi aspetto di restituire 404, usando i matcher RSpec e 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

Questa sana paranoia mi ha permesso di individuare la discrepanza del tipo di contenuto quando tutto il resto sembrava peachy :) Controllo tutti questi elementi: variabili assegnate, codice di risposta, tipo di contenuto di risposta, modello reso, layout reso, messaggi flash.

Salterò il controllo del tipo di contenuto su applicazioni strettamente HTML ... a volte. Dopotutto, "uno scettico controlla TUTTI i cassetti" :)

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

Cordiali saluti: Non raccomando di testare cose che stanno accadendo nel controller, cioè "should_raise". Quello che ti interessa è l'output. I miei test sopra mi hanno permesso di provare varie soluzioni, e i test rimangono gli stessi sia che la soluzione stia sollevando un'eccezione, un rendering speciale, ecc.


3
piace molto questa risposta, soprattutto per quanto riguarda il test dell'output e non i metodi chiamati nel controller ...
xentek,

Rails è dotato di 404 stato: render :text => 'Not Found', :status => :not_found.
Lasse Bunk,

1
@JaimeBellmyer - Sono certo che non restituisce un 200 quando ci si trova in un ambiente distribuito (ad es. Messa in scena / prod). Lo faccio in diverse applicazioni e funziona come descritto nella soluzione accettata. Forse a cui ti riferisci è che restituisce un 200 quando esegue il rendering della schermata di debug in sviluppo dove probabilmente hai il config.consider_all_requests_localparametro impostato su true nel tuo environments/development.rbfile. Se si genera un errore, come descritto nella soluzione accettata, nella messa in scena / produzione, si otterrà sicuramente un 404, non un 200.
Javid Jamae

18

È inoltre possibile utilizzare il file di rendering:

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

Dove puoi scegliere di usare il layout o no.

Un'altra opzione è quella di utilizzare le eccezioni per controllarlo:

raise ActiveRecord::RecordNotFound, "Record not found."

13

La risposta selezionata non funziona in Rails 3.1+ poiché il gestore degli errori è stato spostato in un middleware (vedi problema con github ).

Ecco la soluzione che ho trovato di cui sono abbastanza contento.

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

e in application.rb:

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

E nelle mie risorse (mostra, modifica, aggiorna, elimina):

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

Questo potrebbe certamente essere migliorato, ma almeno ho viste diverse per not_found e internal_error senza sovrascrivere le funzioni di base di Rails.


3
questa è una soluzione molto bella; tuttavia, non è necessaria la || not_foundparte, basta chiamare find!(notare il botto) e genererà ActiveRecord :: RecordNotFound quando non è possibile recuperare la risorsa. Inoltre, aggiungi ActiveRecord :: RecordNotFound all'array nella condizione if.
Marek Příhoda,

1
Vorrei salvare StandardErrore non Exception, per ogni evenienza. In realtà lascerò la pagina statica 500 standard e non userò affatto l'abitudine render_500, il che significa che esplicitamente rescue_fromschiererò una serie di errori relativi a 404
Dr.Strangelove,

7

questi ti aiuteranno ...

Controller applicazione

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

Controller errori

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

views / errori / 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

3
<%= render file: 'public/404', status: 404, formats: [:html] %>

basta aggiungere questo alla pagina che si desidera rendere alla pagina di errore 404 e il gioco è fatto.


1

Volevo lanciare un 404 "normale" per qualsiasi utente connesso che non fosse un amministratore, quindi ho finito per scrivere qualcosa di simile 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

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

0

Per testare la gestione degli errori, puoi fare qualcosa del genere:

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

0

Se vuoi gestire 404 diversi in modi diversi, prendi in considerazione di catturarli nei tuoi controller. Ciò ti consentirà di fare cose come tracciare il numero di 404 generati da diversi gruppi di utenti, avere il supporto interagire con gli utenti per scoprire cosa è andato storto / quale parte dell'esperienza utente potrebbe aver bisogno di modifiche, fare test A / B, ecc.

Ho inserito la logica di base in ApplicationController, ma può anche essere posizionata in controller più specifici, per avere una logica speciale solo per un controller.

Il motivo per cui sto usando un if con ENV ['RESCUE_404'], è così che posso testare il sollevamento di AR :: RecordNotFound in isolamento. Nei test, posso impostare questo var ENV su false e il mio rescue_from non si attiva. In questo modo posso testare il sollevamento separato dalla logica 404 condizionale.

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.