¿Cómo redirigir a un 404 en Rails?

482

Me gustaría 'falsificar' una página 404 en Rails. En PHP, simplemente enviaría un encabezado con el código de error como tal:

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

¿Cómo se hace eso con Rails?

Yuval Karmi
fuente

Respuestas:

1049

No renderice 404 usted mismo, no hay razón para hacerlo; Rails tiene esta funcionalidad incorporada ya. Si desea mostrar una página 404, cree un render_404método (o not_foundcomo lo llamé) de ApplicationControlleresta manera:

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

Los rieles también se manejan AbstractController::ActionNotFound, y de ActiveRecord::RecordNotFoundla misma manera.

Esto hace dos cosas mejor:

1) Utiliza el rescue_fromcontrolador integrado de Rails para representar la página 404, y 2) interrumpe la ejecución de su código, permitiéndole hacer cosas agradables como:

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

sin tener que escribir declaraciones condicionales feas.

Como beneficio adicional, también es muy fácil de manejar en las pruebas. Por ejemplo, en una prueba de integración 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)

Y minitest:

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

O consulte más información de Rails render 404 no encontrado desde una acción del controlador

Steven Soroka
fuente
3
Hay una razón para hacerlo usted mismo. Si su aplicación secuestra todas las rutas desde la raíz. Es un mal diseño, pero a veces no se puede evitar.
ablemike
77
Este enfoque también le permite usar los buscadores de explosión de ActiveRecord (find !, find_by _...!, Etc.), que generan una excepción ActiveRecord :: RecordNotFound si no se encuentra ningún registro (que activa el controlador rescue_from).
gjvis
2
Esto genera un error interno del servidor 500 para mí, no un 404. ¿Qué me estoy perdiendo?
Glenn
3
Parece que ActionController::RecordNotFoundes la mejor opción?
Peter Ehrlich
44
El código funcionó muy bien, pero la prueba no funcionó hasta que me di cuenta de que estaba usando RSpec 2, que tiene una sintaxis diferente: expect { visit '/something/you/want/to/404' }.to raise_error(ActionController::RoutingError)/ via stackoverflow.com/a/1722839/993890
ryanttb
243

HTTP 404 Status

Para devolver un encabezado 404, simplemente use la :statusopción para el método de representación.

def action
  # here the code

  render :status => 404
end

Si desea representar la página 404 estándar, puede extraer la característica en un método.

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

y llámalo en tu acción

def action
  # here the code

  render_404
end

Si desea que la acción represente la página de error y se detenga, simplemente use una declaración de devolución.

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

  # here the code that will never be executed
end

ActiveRecord y HTTP 404

Recuerde también que Rails rescata algunos errores de ActiveRecord, como ActiveRecord::RecordNotFoundmostrar la página de error 404.

Significa que no necesita rescatar esta acción usted mismo

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

User.findplantea un ActiveRecord::RecordNotFoundcuando el usuario no existe. Esta es una característica muy poderosa. Mira el siguiente código

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

Puede simplificarlo delegando en Rails el cheque. Simplemente use la versión de explosión.

def show
  user = User.find_by_email!(params[:email])
  # ...
end
Simone Carletti
fuente
99
Hay un gran problema con esta solución; seguirá ejecutando el código en la plantilla. Entonces, si tiene una estructura simple y tranquila y alguien ingresa una ID que no existe, su plantilla buscará el objeto que no existe.
jcalvert
55
Como se mencionó anteriormente, esta no es la respuesta correcta. Prueba el de Steven.
Pablo Marambio
Cambió la respuesta seleccionada para reflejar la mejor práctica. ¡Gracias por los comentarios chicos!
Yuval Karmi
1
Actualicé la respuesta con más ejemplos y una nota sobre ActiveRecord.
Simone Carletti
1
La versión bang sí detiene la ejecución del código, por lo que es la solución más efectiva en mi humilde opinión.
Gui vieira
60

La respuesta recientemente seleccionada presentada por Steven Soroka es cercana, pero no completa. La prueba en sí misma oculta el hecho de que esto no está devolviendo un verdadero 404, está devolviendo un estado de 200 - "éxito". La respuesta original estaba más cerca, pero intentó renderizar el diseño como si no hubiera ocurrido ninguna falla. Esto arregla todo:

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

Aquí hay un conjunto de pruebas típico mío para algo que espero devolver 404, usando los emparejadores RSpec y 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

Esta paranoia saludable me permitió detectar la falta de coincidencia de tipo de contenido cuando todo lo demás parecía color de rosa :) Compruebo todos estos elementos: variables asignadas, código de respuesta, tipo de contenido de respuesta, plantilla renderizada, diseño renderizado, mensajes flash.

Omitiré la verificación del tipo de contenido en aplicaciones que son estrictamente html ... a veces. Después de todo, "un escéptico revisa TODOS los cajones" :)

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

FYI: No recomiendo probar las cosas que están sucediendo en el controlador, es decir, "debería_raise". Lo que te importa es la salida. Mis pruebas anteriores me permitieron probar varias soluciones, y las pruebas siguen siendo las mismas si la solución está generando una excepción, una representación especial, etc.

Jaime Bellmyer
fuente
3
Realmente me gusta esta respuesta, especialmente con respecto a la prueba de la salida y no a los métodos llamados en el controlador ...
xentek
Raíles ha incorporado en el estado 404: render :text => 'Not Found', :status => :not_found.
Lasse Bunk
1
@JaimeBellmyer: estoy seguro de que no devuelve un 200 cuando estás en un entorno implementado (es decir, provisional / prod). Hago esto en varias aplicaciones y funciona como se describe en la solución aceptada. Quizás a lo que te refieres es que devuelve un 200 cuando muestra la pantalla de depuración en desarrollo, donde probablemente tengas el config.consider_all_requests_localparámetro establecido en verdadero en tu environments/development.rbarchivo. Si genera un error, como se describe en la solución aceptada, en la puesta en escena / producción, definitivamente obtendrá un 404, no un 200.
Javid Jamae
18

También puede usar el archivo de renderizado:

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

Donde puede elegir usar el diseño o no.

Otra opción es usar las Excepciones para controlarlo:

raise ActiveRecord::RecordNotFound, "Record not found."
Paulo Fidalgo
fuente
13

La respuesta seleccionada no funciona en Rails 3.1+ ya que el controlador de errores se movió a un middleware (vea el problema de github ).

Aquí está la solución que encontré con la que estoy bastante contento.

En 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

y en application.rb:

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

Y en mis recursos (mostrar, editar, actualizar, eliminar):

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

Esto ciertamente podría mejorarse, pero al menos, tengo diferentes vistas para not_found e internal_error sin anular las funciones principales de Rails.

Augustin Riedinger
fuente
3
esta es una muy buena solución; sin embargo, no necesita la || not_foundparte, solo llame find!(note la explosión) y arrojará ActiveRecord :: RecordNotFound cuando el recurso no se pueda recuperar. Además, agregue ActiveRecord :: RecordNotFound a la matriz en la condición if.
Marek Příhoda
1
Yo rescataría StandardErrory no Exception, por si acaso. En realidad, dejaré la página estática estándar 500 y no render_500rescue_from
usaré
7

estos te ayudarán ...

Controlador de aplicaciones

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

Controlador de errores

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

vistas / errores / 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
fuente
3
<%= render file: 'public/404', status: 404, formats: [:html] %>

simplemente agregue esto a la página que desea representar en la página de error 404 y ya está.

Ahmed Reza
fuente
1

Quería lanzar un 404 'normal' para cualquier usuario conectado que no sea administrador, así que terminé escribiendo algo como esto en 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
paredes vacias
fuente
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
fuente
0

Para probar el manejo de errores, puede hacer algo como esto:

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
fuente
0

Si desea manejar diferentes 404 de diferentes maneras, considere atraparlos en sus controladores. Esto le permitirá hacer cosas como rastrear la cantidad de 404 generados por diferentes grupos de usuarios, tener soporte para interactuar con los usuarios para averiguar qué salió mal / qué parte de la experiencia del usuario podría necesitar ajustes, hacer pruebas A / B, etc.

He colocado aquí la lógica base en ApplicationController, pero también se puede colocar en controladores más específicos, para tener una lógica especial solo para un controlador.

La razón por la que estoy usando un if con ENV ['RESCUE_404'] es para poder probar el aumento de AR :: RecordNotFound de forma aislada. En las pruebas, puedo establecer esta var ENV en falso, y mi rescate_desde no se disparará. De esta manera puedo probar la subida por separado de la lógica 404 condicional.

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
fuente