Como redirecionar para um 404 no Rails?

482

Eu gostaria de 'falsificar' uma página 404 no Rails. No PHP, eu apenas enviava um cabeçalho com o código de erro da seguinte forma:

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

Como isso é feito com o Rails?

Yuval Karmi
fonte

Respostas:

1049

Não faça 404 você mesmo, não há motivo para; O Rails já possui essa funcionalidade. Se você deseja mostrar uma página 404, crie um render_404método (ou not_foundcomo eu o chamei) ApplicationControllerassim:

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

Os trilhos também manipulam AbstractController::ActionNotFound, e ActiveRecord::RecordNotFoundda mesma maneira.

Isso faz duas coisas melhor:

1) Ele usa o rescue_frommanipulador embutido do Rails para renderizar a página 404 e 2) interrompe a execução do seu código, permitindo que você faça coisas legais como:

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

sem ter que escrever declarações condicionais feias.

Como bônus, também é super fácil de manusear nos testes. Por exemplo, em um teste de integração 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 minitest:

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

OU consulte mais informações do Rails render 404 não encontrado em uma ação do controlador

Steven Soroka
fonte
3
Há uma razão para fazer você mesmo. Se o seu aplicativo seqüestrar todas as rotas da raiz. É um design ruim, mas às vezes inevitável.
ablemike
7
Essa abordagem também permite que você use os localizadores de retorno do ActiveRecord (encontre !, encontre_ por _...!, Etc.), que geram uma exceção ActiveRecord :: RecordNotFound se nenhum registro for encontrado (acionando o manipulador rescue_from).
gjvis
2
Isso gera um erro interno de 500 servidores para mim, não um 404. O que estou perdendo?
Glenn
3
Parece que ActionController::RecordNotFoundé a melhor opção?
Peter Ehrlich
4
O código funcionou muito bem, mas o teste não foi até que eu percebi que eu estava usando RSpec 2, que tem uma sintaxe diferente: expect { visit '/something/you/want/to/404' }.to raise_error(ActionController::RoutingError)/ via stackoverflow.com/a/1722839/993890
ryanttb
243

Status HTTP 404

Para retornar um cabeçalho 404, basta usar a :statusopção para o método de renderização.

def action
  # here the code

  render :status => 404
end

Se você deseja renderizar a página 404 padrão, pode extrair o recurso em um 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

e chame na sua ação

def action
  # here the code

  render_404
end

Se você deseja que a ação renderize a página de erro e pare, basta usar uma declaração de retorno.

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

  # here the code that will never be executed
end

ActiveRecord e HTTP 404

Lembre-se também de que o Rails recupera alguns erros do ActiveRecord, como a ActiveRecord::RecordNotFoundexibição da página de erro 404.

Isso significa que você não precisa resgatar essa ação sozinho

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

User.findgera um ActiveRecord::RecordNotFoundquando o usuário não existe. Este é um recurso muito poderoso. Veja o seguinte código

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

Você pode simplificá-lo delegando ao Rails a verificação. Basta usar a versão estrondosa.

def show
  user = User.find_by_email!(params[:email])
  # ...
end
Simone Carletti
fonte
9
Há um grande problema com esta solução; ainda executará o código no modelo. Portanto, se você tiver uma estrutura simples e tranquila e alguém inserir um ID que não existe, seu modelo procurará o objeto que não existe.
jcalvert
5
Como mencionado anteriormente, esta não é a resposta correta. Tente o de Steven.
Pablo Marambio 07/07
A resposta selecionada foi alterada para refletir a melhor prática. Obrigado pelos comentários, pessoal!
Yuval Karmi
1
Atualizei a resposta com mais exemplos e uma observação sobre o ActiveRecord.
Simone Carletti 25/10
1
A versão bang interrompe a execução do código, por isso é a solução mais eficaz IMHO.
Gui Vieira
60

A resposta recém selecionada por Steven Soroka está próxima, mas não completa. O teste em si oculta o fato de que isso não está retornando um 404 verdadeiro - está retornando um status de 200 - "sucesso". A resposta original estava mais próxima, mas tentou renderizar o layout como se nenhuma falha tivesse ocorrido. Isso corrige tudo:

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

Aqui está um conjunto de testes típico para algo que espero retornar 404, usando os correspondentes 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

Essa paranóia saudável me permitiu identificar a incompatibilidade do tipo de conteúdo quando tudo parecia pêssego :) Verifico todos esses elementos: variáveis ​​atribuídas, código de resposta, tipo de conteúdo de resposta, modelo renderizado, layout renderizado, mensagens em flash.

Vou pular a verificação do tipo de conteúdo em aplicativos estritamente html ... às vezes. Afinal, "um cético verifica TODAS as gavetas" :)

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

FYI: Eu não recomendo testar coisas que estão acontecendo no controlador, ou seja, "should_raise". O que importa é a saída. Meus testes acima me permitiram tentar várias soluções, e os testes permanecem os mesmos, independentemente de a solução estar gerando uma exceção, renderização especial etc.

Jaime Bellmyer
fonte
3
realmente gosto desta resposta, especialmente com relação ao teste da saída e não os métodos chamados no controlador ...
xentek
Rails foi construído com 404 status: render :text => 'Not Found', :status => :not_found.
precisa
1
@ JaimeBellmyer - Tenho certeza de que ele não retorna 200 quando você está em um ambiente implantado (por exemplo, estadiamento / produção). Eu faço isso em várias aplicações e funciona como descrito na solução aceita. Talvez o que você está se referindo é que ele retorna 200 quando renderiza a tela de depuração em desenvolvimento, onde você provavelmente tem o config.consider_all_requests_localparâmetro definido como true no seu environments/development.rbarquivo. Se você gerar um erro, como descrito na solução aceita, no estadiamento / produção, você vai definitivamente ter um 404, não um 200.
Javid Jamae
18

Você também pode usar o arquivo de renderização:

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

Onde você pode optar por usar o layout ou não.

Outra opção é usar as exceções para controlá-lo:

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

A resposta selecionada não funciona no Rails 3.1+, pois o manipulador de erros foi movido para um middleware (consulte a edição do github ).

Aqui está a solução que encontrei com a qual estou muito feliz.

Em 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 em application.rb:

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

E nos meus recursos (mostrar, editar, atualizar, excluir):

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

Isso certamente poderia ser melhorado, mas pelo menos, tenho visões diferentes para not_found e internal_error sem substituir as principais funções do Rails.

Augustin Riedinger
fonte
3
esta é uma solução muito boa; no entanto, você não precisa da || not_foundpeça, basta chamar find!(observe o estrondo) e ela lançará ActiveRecord :: RecordNotFound quando o recurso não puder ser recuperado. Além disso, adicione ActiveRecord :: RecordNotFound à matriz na condição if.
Marek Příhoda
1
Eu resgataria StandardErrore não Exception, apenas por precaução. Na verdade, eu vou deixar página 500 estática padrão e não usar personalizado render_500em tudo, o que significa que eu vou explicitamente rescue_fromsérie de erros relacionados a 404
Dr.Strangelove
7

estes irão ajudá-lo ...

Controlador de Aplicação

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 erros

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

visualizações / erros / 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
fonte
3
<%= render file: 'public/404', status: 404, formats: [:html] %>

basta adicionar isso à página que você deseja renderizar à página de erro 404 e pronto.

Ahmed Reza
fonte
1

Eu queria lançar um 404 'normal' para qualquer usuário conectado que não seja um administrador, então acabei escrevendo algo parecido com isso no 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 vazias
fonte
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
fonte
0

Para testar o tratamento de erros, você pode fazer algo assim:

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

Se você quiser lidar com 404s diferentes de maneiras diferentes, considere capturá-los em seus controladores. Isso permitirá que você faça coisas como rastrear o número de 404s gerados por diferentes grupos de usuários, tenha suporte para interagir com os usuários para descobrir o que deu errado / que parte da experiência do usuário pode precisar de ajustes, fazer testes A / B etc.

Coloquei aqui a lógica básica no ApplicationController, mas também pode ser colocada em controladores mais específicos, para ter lógica especial apenas para um controlador.

O motivo pelo qual estou usando um if com ENV ['RESCUE_404'] é para poder testar o aumento de AR :: RecordNotFound isoladamente. Nos testes, eu posso definir esse ENV var como false e meu rescue_from não dispara. Dessa forma, posso testar o aumento separado da 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
fonte